Pythonを使って日本語の要約データを取得する

スポンサーリンク

 
自然言語処理のタスクは、Transformer が現れて以来一段と盛り上がっています。これまで精度がいまいちだったタスクで、人間以上の精度になってきています。それは、文章の要約タスクでも同様です。文章要約は、与えれた文章の中で重要なワードや文を抜き取り、場合によっては言い換え、意味がわかる自然な文章を作るタスクです。ただ文章を短くすればよいというわけではなく、意味がわかるようにまとめなければいけないというところが、他の自然言語処理のタスクと比べて難しい所以です。


精度の高い要約モデルを作るためには、莫大なデータで事前学習済みのモデルを fine-tuning します。しかし、日本語の文章と要約が対になっている要約データは、簡単に用意できません。そこで、今回は日本語の要約データを取得するスクリプトに関して記載しました。


今回のコードはこちらに置いています。



準備


今回取得する要約データは、livedoor ニュースの3行要約データです。下記の GitHub では、 livedoor ニュースのニュース ID データがあります。まずはこのデータを取得します。


github.com


必要なモジュールの Import とディレクトリの作成


スクレイピングをして要約データを取得するため、「requests」と「bs4」をインポートしています。また、取得したデータを保存するディレクトリ「data」を作っています。

from pathlib import Path
import re
import time
import requests
import pandas as pd
import urllib
from bs4 import BeautifulSoup
from tqdm import tqdm

data_dir_path = Path('data')
if not data_dir_path.exists():
    data_dir_path.mkdir(parents=True)



ニュースIDデータの取得


このディレクトリにあるニュース ID データの「train.csv」と「test.csv」をダウンロードします。

def download_data(url, data_dir_path):
    
    file_path = data_dir_path.joinpath(Path(url).name)

    data = requests.get(url).content
    with open(file_path, 'wb') as file:
        file.write(data)

url = 'https://raw.githubusercontent.com/KodairaTomonori/ThreeLineSummaryDataset/master/data/train.csv'
download_data(url=url, data_dir_path=data_dir_path)

url = 'https://raw.githubusercontent.com/KodairaTomonori/ThreeLineSummaryDataset/master/data/test.csv'
download_data(url=url, data_dir_path=data_dir_path)



ニュースIDデータの加工


取得したニュース ID データ2つをロードし、連結させます。その後、ニュース ID の不要な文字を消し、すでにニュース本文を取得しているニュース ID があれば、「anti_join関数」を使いそれを省くようにしています。

def anti_join(data1, data2, by):

    joined_data = data1.copy()
    target_data = data2.copy()
    target_data['flag_tmp'] = 1

    if type(by) is str:
        by = [by]

    joined_data = pd.merge(
        joined_data, target_data[by + ['flag_tmp']].drop_duplicates(),
        on=by, how='left'
    ).query('flag_tmp.isnull()', engine='python').drop(columns='flag_tmp').copy()

    return joined_data

columns = ['year', 'month', 'category', 'article_id', 'type_label']

articles = pd.DataFrame()
for data_name in ['train.csv', 'test.csv']:
    data = pd.read_csv(data_dir_path.joinpath(data_name))
    tmp = data.columns.tolist()
    if data_name == 'train.csv':
        data.columns = columns[:-1]
        data = pd.concat([
            data, pd.DataFrame([tmp], columns=columns[:-1])
        ], axis=0)
        data['type_label'] = None
    else:
        data.columns = columns
        data = pd.concat([
            data, pd.DataFrame([tmp], columns=columns)
        ], axis=0)
        
    articles = pd.concat([articles, data], axis=0)

articles = articles.assign(
    year=lambda x: x.year.astype(int),
    article_id=lambda x: x.article_id.map(lambda y: re.sub(r'[a-z\.]', '', str(y))).astype(int)
)

if body_data_file_path.exists():
    articles = anti_join(
        articles,
        pd.read_csv(body_data_file_path).assign(article_id=lambda x: x.article_id.astype(int)),
        by='article_id'
    )



ニュース本文と要約を取得


上記の処理でニュース ID データの取得と加工が完了しました。このデータのニュース ID のニュース本文と要約をスクレイピングします。この際、サーバー処理に負荷をかけないように、スクレイピングするたびに「waiting_time」で指定した時間の処理待ちをしています。また、取得件数を指定しているのが「n_writing_data」です。

waiting_time = 3              # スクレイピングの間隔
n_writing_data = 10000        # 取得する件数 
article_url = 'http://news.livedoor.com/article/detail/{}/'
body_data_file_path = data_dir_path.joinpath('body_data.csv')
summary_data_file_path = data_dir_path.joinpath('summary_data.csv')

target_articles = articles.sort_values(
    'year', ascending=False
).head(min(len(articles), n_writing_data))



ニュース本文と要約をスクレイピング


加工したニュース ID データから1件ずつスクレイピングし、ニュース本文と要約を獲得します。取得できなかった ID も残し、再度スクレイピングする際に、取得できなかった ID はスクレイピングしないようにします。途中で処理落ちする事を考慮し、50件ずつ保存する処理も入れています。

def read_url_to_soup(url):
    
    try:
        response = urllib.request.urlopen(url)
        html = response.read().decode(response.headers.get_content_charset(), errors='ignore')
        soup = BeautifulSoup(html, 'html.parser')
    except Exception:
        soup = None
    
    return soup

def write_data(data, file_path):
    if file_path.exists():
        data = pd.concat([data, pd.read_csv(file_path)]).assign(
            article_id=lambda x: x.article_id.astype(int)
        ).drop_duplicates()
    data.to_csv(file_path, index=False)

body_data = []
summary_data = []
i = 1
for article_id in tqdm(target_articles['article_id']):

    url = article_url.format(article_id)

    soup = read_url_to_soup(url)
    if soup is None or soup.find(class_='articleBody') is None or soup.find(class_='summaryList') is None:
        body_data.append((article_id, None, None))
        summary_data.append((article_id, None))
    else:

        title = soup.find(id='article-body').find('h1').text.strip()

        body = soup.find(class_='articleBody').find('span', {'itemprop': 'articleBody'}).text
        body = re.sub('\n+', '\n', body)
        body_data.append((article_id, title, body))

        summary_list = soup.find(class_='summaryList').find_all('li')
        summary_list = list(map(lambda x: x.text.strip(), summary_list))

        summary_data.extend([(article_id, summary) for summary in summary_list])
    
    if i % 50 == 0:        
        body_data = pd.DataFrame(body_data, columns=['article_id', 'title', 'text'])
        summary_data = pd.DataFrame(summary_data, columns=['article_id', 'text'])
        write_data(data=body_data, file_path=body_data_file_path)
        write_data(data=summary_data, file_path=summary_data_file_path)
        body_data = []
        summary_data = []        

    i += 1
    time.sleep(waiting_time)

if len(body_data) > 0:
    body_data = pd.DataFrame(body_data, columns=['article_id', 'title', 'text'])
    summary_data = pd.DataFrame(summary_data, columns=['article_id', 'text'])
    write_data(data=body_data, file_path=body_data_file_path)
    write_data(data=summary_data, file_path=summary_data_file_path)

取得したデータの確認


取得したデータをロードして確認します。

data = pd.read_csv(body_data_file_path)
data.head()

f:id:dskomei:20211012214411p:plain:w600


data = pd.read_csv(summary_data_file_path)
data.head()

f:id:dskomei:20211012214511p:plain:w500


意図したデータを取得できていることがわかります。