📑

【Team JINIAC】官公庁データセットの収集(Webスクレイピングによる事後学習用データセット作成)

2024/08/21に公開

この記事でわかること

この記事では、日本語LLM開発のために、各省庁の大臣記者会見の質疑応答をスクレイピングしてSFT用データセットを作成する方法について解説します。具体的には、以下の内容がわかります。

  • 各省庁の記者会見の質疑応答データのスクレイピング方法
  • スクレイピングしたデータをJSONL形式で保存する方法
  • データセットに’instruction’列を追加して整形する方法
  • 実際のスクレイピングコードの例

はじめに

私たち(Team JINIAC)では、日本語LLM開発のためのSFT用データセットを作成するにあたり、高品質なQAデータセットとして各省庁の大臣記者会見の質疑応答に着目し、それらをWebスクレイピングにより収集してSFTデータセットとして整形しましたので、その工程をご紹介します。

作業のアウトライン

今回作成したコードは、各省庁記者会見のうち質疑応答部分だけをスクレイピングし、データセットとして整形するところまでを実行するものです。具体的には、以下のような処理を行っています。

  1. 各省庁の記者会見の質疑応答が記録されているウェブページからデータを取得(スクレイピング)します。
  2. 取得したデータを、‘URL’, ‘質問’, '回答’の形式のJSONLファイルとして保存します。
  3. さらに、各質問と回答に対応する省庁名を示す新しい列’instruction’を追加します。この’instruction’列の値は、「●●省の記者会見における質問と回答」という形式の文字列です。
  4. 最後に、更新されたデータを同じJSONLファイルに上書き保存します。

このデータセットは日本語LLMの事後学習(SFT)向けに作成するものですので、「質問」「回答」だけでなく、「instruction」を追加しています。また、データソースを明示するため、「URL」も併せて入れておくようにしました。

また、各省庁のwebサイトを調べたところ、省庁ごとにHTML構造が異なるため、それぞれ異なるスクレイピングコードを整備しました。今回対象としたのは、総務省、国土交通省、厚生労働省、文部科学省、農林水産省、金融庁の6省庁です。

データセットの形式

前述のとおり、スクレイピングしたデータは’URL’, ‘instruction’, ‘質問’, '回答’の形式のJsonlファイルとして出力しました。

スクレイピングコードの例

一例ですが、国土交通省サイトからのスクレイピングコードは次のとおりです。

!pip install requests beautifulsoup4

import requests
from bs4 import BeautifulSoup
import time
from urllib.parse import urljoin
import json
import re

# スクレイピング対象のURL
url = "https://www.mlit.go.jp/report/interview/daijin.html"

# URLからHTMLを取得
response = requests.get(url)
response.encoding = response.apparent_encoding  # 文字化け対策

# BeautifulSoupオブジェクトを作成
soup = BeautifulSoup(response.text, 'html.parser')

# divタグのclass="menuList01"の中のaタグを取得
a_tags = soup.select('div.menuList01 ul li div p a')

# 各aタグのhref属性の値(リンク先)を取得
links = [a.get('href').replace('\r', '') for a in a_tags]  # '\r'を削除

# リンク先を出力
for link in links:
    print("'" + link + "',")

def scrape_links(url):
    """
    指定されたURLからリンクを抽出します。

    Args:
        url (str): 処理対象のURL

    Returns:
        list: 抽出されたリンクのリスト
    """

    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    div = soup.find('div', {'class': 'pressList01'})

    links = []
    if div is not None:
        links = [urljoin(url, a.get('href')) for a in div.find_all('a') if a.get('href')]
    return links

def scrape_qa(url):
    """
    指定されたURLからQAデータを抽出します。

    Args:
        url (str): 処理対象のURL

    Returns:
        list: 抽出されたQAデータのリスト
    """

    response = requests.get(url)
    response.encoding = response.apparent_encoding  # 文字化け対策

    soup = BeautifulSoup(response.text, 'html.parser')
    divs = soup.find_all('div', {'class': 'section'})

    qa_list = []

    for div in divs:
        if div is not None:
            text = div.text.strip()
            if '(問)' in text:
                split_text = re.split('(問)', text)[1:]  # Skip the first split as it is before the first question
                for i in range(len(split_text)):
                    question_answer_split = re.split('(答)', split_text[i], 1)
                    question = question_answer_split[0].strip()
                    answer = question_answer_split[1].strip() if len(question_answer_split) > 1 else ""
                    qa_list.append({'URL': url, '質問': question, '回答': answer})
                    time.sleep(1)

    # Remove duplicates
    qa_list = [dict(t) for t in set(tuple(d.items()) for d in qa_list)]
    return qa_list

# URLを指定して関数を呼び出す
urls = [
   'https://www.mlit.go.jp/report/interview/R3daijin.html',
   'https://www.mlit.go.jp/report/interview/R2daijin.html',
   'https://www.mlit.go.jp/report/interview/H31daijin.html',
   'https://www.mlit.go.jp/report/interview/H30daijin.html',
   'https://www.mlit.go.jp/report/interview/H29daijin.html',
   'https://www.mlit.go.jp/report/interview/H28daijin.html',
   'https://www.mlit.go.jp/report/interview/H27daijin.html',
   'https://www.mlit.go.jp/report/interview/H26daijin.html',
   'https://www.mlit.go.jp/report/interview/H25daijin.html',
   'https://www.mlit.go.jp/report/interview/H24daijin.html',
   'https://www.mlit.go.jp/report/interview/H23daijin.html',
   'https://www.mlit.go.jp/report/interview/H22daijin.html',
   'https://www.mlit.go.jp/report/interview/H21daijin.html',
   'https://www.mlit.go.jp/report/interview/kanekodaijin08.html',
   'http://www.mlit.go.jp/kaiken/kaiken08/history.html'
    ]

# JSON形式で出力
with open('QA_milt.jsonl', 'a') as f:
    for url in urls:
        links = scrape_links(url)
        for link in links:
            qa_list = scrape_qa(link)
            #print(qa_list)
            for item in qa_list:
                f.write(json.dumps(item, ensure_ascii=False) + '\n')

上記コードの内容は次のとおりです。

ライブラリのインポート

import requests
from bs4 import BeautifulSoup
import time
from urllib.parse import urljoin
import json
import re

ここでは、必要なPythonモジュールをインポートしています。requestsはHTTPリクエストを送るためのモジュール、BeautifulSoupはHTMLやXMLの解析を行うためのモジュール、timeは時間に関する操作を行うためのモジュール、urljoinはURLを結合するための関数、jsonはJSON形式のデータを扱うためのモジュール、reは正規表現を扱うためのモジュールです。

スクレイピング対象のURLからHTMLを取得

# スクレイピング対象のURL
url = "https://www.mlit.go.jp/report/interview/daijin.html"

# URLからHTMLを取得
response = requests.get(url)
response.encoding = response.apparent_encoding  # 文字化け対策

# BeautifulSoupオブジェクトを作成
soup = BeautifulSoup(response.text, 'html.parser')

この部分では、スクレイピング対象のURLからHTMLを取得し、そのHTMLを解析するためのBeautifulSoupオブジェクトを作成しています。response.apparent_encodingを使うことで、文字化けを防ぐことができます。

リンクの取得

# divタグのclass="menuList01"の中のaタグを取得
a_tags = soup.select('div.menuList01 ul li div p a')

# 各aタグのhref属性の値(リンク先)を取得
links = [a.get('href').replace('\r', '') for a in a_tags]  # '\r'を削除

# リンク先を出力
for link in links:
    print("'" + link + "',")

この部分では、取得したHTMLから特定のaタグ(リンク)を取得し、そのリンク先(href属性の値)を取得しています。具体的には、divタグのclass属性が"menuList01"である要素の中のaタグを取得しています。そして、各aタグのhref属性の値から改行文字(\r)を削除し、その結果をリンクのリストとして保存しています。

スクレイピング関数の定義

def scrape_links(url):
    ...
def scrape_qa(url):
    ...

この部分では、リンクの抽出とQAデータの抽出を行う2つの関数を定義しています。scrape_links関数は指定されたURLからリンクを抽出し、scrape_qa関数は指定されたURLからQAデータを抽出します。

スクレイピングの実行と結果の保存

# URLを指定して関数を呼び出す
urls = [
   'https://www.mlit.go.jp/report/interview/R3daijin.html',
   'https://www.mlit.go.jp/report/interview/R2daijin.html',
   'https://www.mlit.go.jp/report/interview/H31daijin.html',
   'https://www.mlit.go.jp/report/interview/H30daijin.html',
   'https://www.mlit.go.jp/report/interview/H29daijin.html',
   'https://www.mlit.go.jp/report/interview/H28daijin.html',
   'https://www.mlit.go.jp/report/interview/H27daijin.html',
   'https://www.mlit.go.jp/report/interview/H26daijin.html',
   'https://www.mlit.go.jp/report/interview/H25daijin.html',
   'https://www.mlit.go.jp/report/interview/H24daijin.html',
   'https://www.mlit.go.jp/report/interview/H23daijin.html',
   'https://www.mlit.go.jp/report/interview/H22daijin.html',
   'https://www.mlit.go.jp/report/interview/H21daijin.html',
   'https://www.mlit.go.jp/report/interview/kanekodaijin08.html',
   'http://www.mlit.go.jp/kaiken/kaiken08/history.html'
    ]

# JSON形式で出力
with open('QA_milt.jsonl', 'a') as f:
    for url in urls:
        links = scrape_links(url)
        for link in links:
            qa_list = scrape_qa(link)
            #print(qa_list)
            for item in qa_list:
                f.write(json.dumps(item, ensure_ascii=False) + '\n')

この部分では、スクレイピングを実行し、その結果をJSON形式でファイルに保存しています。具体的には、指定された各URLに対してscrape_links関数を呼び出し、取得した各リンクに対してscrape_qa関数を呼び出しています。そして、取得したQAデータをJSON形式の文字列に変換し、それをファイルに書き込んでいます。

instruction列の追加、整形

日本語LLMの事後学習用データセットの作成を目的としているため、スクレイピングしたデータに’instruction’列を追加しました。

以下に、google Colabのコードを示します。


import json
import pandas as pd
from google.colab import drive

# Google Driveをマウント
drive.mount('/content/drive')

# ファイル名と対応する省庁名の辞書
files = {
    'QA_mhlw.jsonl': '厚生労働省',
    'QA_maff.jsonl': '農林水産省',
    'QA_milt.jsonl': '国土交通省',
    'QA_soumu.jsonl': '総務省',
    'QA_fsa.jsonl': '金融庁',
    'QA_monka.jsonl': '文部科学省'
}

# 各ファイルに対して処理を行う
for filename, ministry in files.items():
    path = f'/content/drive/MyDrive/datasets/QA/{filename}'  # ファイルのパスを適宜変更してください
    with open(path, 'r') as f:
        data = [json.loads(line) for line in f]

    # 新しい'instruction'列を追加
    for item in data:
        item['instruction'] = f'{ministry}の記者会見における質問と回答'

    # DataFrameとして読み込み、列の順序を調整
    df = pd.DataFrame(data)
    df = df[['URL', 'instruction', '質問', '回答']]

    # ファイルに書き戻す
    with open(path, 'w') as f:
        for item in df.to_dict('records'):
            f.write(json.dumps(item, ensure_ascii=False) + '\n')

まとめ

この記事では、各省庁webサイトから記者会見の質疑応答部分をスクレイピングしてデータセットを作成する過程を紹介しました。作成したデータセットは、Huggingface上に公開する予定です。

💡 この成果は、NEDO(国立研究開発法人新エネルギー・産業技術総合開発機構)の助成事業「ポスト5G情報通信システム基盤強化研究開発事業」(JPNP20017)の結果得られたものです。

東大松尾・岩澤研究室 | LLM開発 プロジェクト[GENIAC]

Discussion