Webスクレイピングによるコーパス収集―RNNで文章生成〈3〉

はじめに

前回の記事↓

RNNを使った言語モデルを設計してみた―RNNで文章生成〈2〉

 前回からかなり期間が開いてしまった。。。

 第3回目の今回はコーパス収集の方法について説明していきたいと思います。コーパスには青空文庫にある太宰治の小説群を使いますが、特定の作家のみのデータを一括ダウンロードする方法が見つからなかったので、手っ取り早く自前のプログラムでWebスクレイピング(Webから情報を取得すること)をすることにしました。

Webスクレイピングの手順

 Webスクレイピングのプログラム実装はコチラの本を参考にしました。

Pythonスクレイピングの基本と実践 データサイエンティストのためのWebデータ収集術(Amazon)

 Webから自動で情報を拾ってくるプログラミングをしてみたいという単純な興味から少し前に読んで、サンプルプログラムを作って遊んでいたのですが、まさかこういう使いどころがあるとは。。。むやみにスクレイピングすることは法的に問題もあるそうですが、今回は著作権の切れた小説のダウンロードで、なおかつそれほど負荷もかからないと思うので大丈夫でしょう。

ではWebスクレイピングの手順を説明します。

1.青空文庫の作家ページにアクセス

2.すべての小説のタイトルと図書カードページのリンクを取得

3.小説の図書カードページにアクセス

4.小説のzipファイルをダウンロード

5.すべての小説に対して3,4を繰り返す

下準備

dazai_scraping.py
# HTTPリクエストを送ってページを取得するライブラリ
import requests
# HTMLを扱いやすい形にしてくれるライブラリ
from bs4 import BeautifulSoup
# データベースを操作するライブラリ
import dataset
# urlを結合する関数
from urllib.parse import urljoin
# 作家ページのURL
base_url = 'https://www.aozora.gr.jp/index_pages/person35.html'
db_path = 'sqlite:///C:/.../dazai_in_aozora.db'
# データベースに接続
db = dataset.connect(db_path)
# テーブル作成
books_table = db.create_table('books', primary_id=False)

 Webスクレイピングに使うライブラリや関数のインポートを行った後、データベースに接続してテーブルを作成しています。データベースは操作が簡単なsqliteを使いました。データベースを操作するライブラリには、これまた簡単で使いやすいdatasetを使いました。URLを主キーに使うので、テーブル作成においてprimary_id=Falseを追加しています。

1.作家ページにアクセス

 アクセスするページはコチラです↓

作家別作品リスト:太宰治(青空文庫)

r = requests.get(base_url)
html_soup = BeautifulSoup(r.content, 'html.parser')

 まずrequests.get()で作家ページにGETメソッドのHTTPリクエストを送り、レスポンスを受け取ります。次にレスポンスの中身(r.content)とパーサー(html.parser)を引数にしてBeautifulSoupのインスタンスを作成します。参考にした本の中では第一引数がr.textになっていたのですが、これだと文字化けが発生したのでr.contentに変更しました。テキスト形式かバイナリ形式かという違いがあるようです。

2.小説のタイトルとページリンクを取得

 先ほどのコードと合わせて関数にしました。引数は作家ページのURLであるbase_urlです。

dazai_scraping.py
def scrape_books(base_url):
    r = requests.get(base_url)
    html_soup = BeautifulSoup(r.content, 'html.parser')
    # 作品リストの<li>タグを取得して繰り返し
    for li_tag in html_soup.select('body > ol > li'):
        # 「新字新仮名」以外は取得しない
        if not '新字新仮名' in li_tag.get_text(strip=True):
            continue
        # <a>タグを取得
        li_a = li_tag.find('a')
        if not li_a or not li_a.get('href'):
            continue
        # データベースに保存
        title = li_a.get_text(strip=True)
        url = li_a.get('href')
        books_table.upsert({'title': title,
                            'url': url,
                            'isDownloaded': 0},
                            ['url'])

 作家ページ上では作品がタイトルとともにリスト上に表示されていたので、それら一つ一つからタイトルと図書カードページ(作品ページ)を取得してデータベースに保存しました。ブラウザの「検証」を使って調べたところ、リストのタグである<ol><li>には特にクラス名などが無かったので、セレクタを'body > ol > li'として、上から順にたどってタグを取得しました。古い仮名遣いが入ってくるといろいろと複雑になりそうなので、今回はコーパスの対象を「新字新仮名」のものに絞ることにしました。大半の小説が「新字新仮名」になっているので、入力データの数には問題ないでしょう。

 データベースに保存する際にはtitleurlのほかに、isDownloadedというフィールドも初期値0で追加しています。これはこの後の処理で小説のzipファイルをダウンロードするときに、未ダウンロードのものだけを抽出するために用います。ダウンロードが済んだら値を1にして、多重ダウンロードを防ぎます。また、テーブルの主キーはurlに設定しています。

3.作品ページにアクセス

 アクセスするページはコチラです(「ア、秋」のデータを取得する場合)↓

図書カード:ア、秋(青空文庫)

r = requests.get(url)
html_soup = BeautifulSoup(r.content, 'html.parser')

 作品ページへのアクセスも作家ページのアクセスと同様に行います。requests.get()の引数のurlは図書カードページ(作品ページ)のURLです。

4.作品データをダウンロード

 こちらも先ほどのコードと合わせて関数にしました。引数のurlは作品ページのURL、titleは作品のタイトルです。

dazai_scraping.py
def scrape_book(url, title):
    r = requests.get(url)
    html_soup = BeautifulSoup(r.content, 'html.parser')
    # zipファイルのurlを取得
    zip_a_tag = html_soup.select_one('table.download a[href$=".zip"]')
    if not zip_a_tag or not zip_a_tag.get('href'):
        return
    # urlを結合
    zip_url = urljoin(url, zip_a_tag.get('href'))
    # zipファイルをダウンロード
    filename = 'books_zip/' + title + '.zip'
    r = requests.get(zip_url, stream=True)
    with open(filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024):
            f.write(chunk)
            f.flush()

 zipファイルのURLを参照している<a>タグを取得するためのセレクタは'table.download a[href$=".zip"]'にしました。まずdownloadクラスの<table>タグを見つけて、その中から.zipで終わるURLを参照している<a>タグを見つけるという手順です。そのあとurljoin()を使ってzipファイルの完全なURLを取得します。

 ファイルダウンロードの際の注意点は3つです。一つは、requests.get()の引数のstream=Trueです。大きいファイルを一気にダウンロードしてメモリがパンクすることを防いでくれるそうです。二つめはr.iter_content()の部分。レスポンスの中身(バイナリ形式)をイテレータ(繰り返し可能オブジェクト?)にして、1024バイトずつファイルに書き込んでいきます。三つめが、f.flush()という記述。これはファイルバッファに渡されたデータを直ちにディスクに書き出すためのメソッドらしいです。このあたりはネットで調べた情報をもとにコードを書いたので正直よくわかっていません。。。

5.繰り返し処理

 作品リストを取得するscrape_books()と、個別の作品のファイルをダウンロードするscrape_book()を定義したので、メインの処理を書きます。

dazai_scraping.py
# 「新字新仮名」の作品すべての作品名と作品ページのURLを取得し、データベースへ保存
inp = input('Do you wish to re-scrape the books (y/n)?')
if inp == 'y':
    scrape_books(base_url)
# スクレイプされていない作品ページから、テキストファイルが圧縮されたzipファイルをダウンロード
for book in books_table.find(isDownloaded=0):
    url = urljoin(base_url, book['url'])
    scrape_book(url, book['title'])
    books_table.upsert({'url': book['url'],
                        'isDownloaded': 1},
                        ['url'])
    print(book['title'] + ' has been downloaded')

 まず作品リストをスクレイプするかどうかの確認処理を設けています。次にisDownloaded=0の未ダウンロード作品をテーブルから抽出して、個別にスクレイピングを行っていきます。スクレイピングが完了し、ダウンロードが済んだらisDownloadedの値を1に書き換えてテーブルを更新しています。

 こうしてすべての作品データをダウンロードしたらプログラムは終了です。

おわりに

 無事に作品データをダウンロードできたので、次は今回集めた作品に入力データとして使用するための前処理を行っていきます。

 最後に青空文庫さん、およびその入力や校正を行っている方々の努力に感謝します。

次回の記事↓

コーパスの整形とSudachiによる分かち書き―RNNで文章生成〈4〉