はじめに
前回の記事↓
RNNを使った言語モデルを設計してみた―RNNで文章生成〈2〉
前回からかなり期間が開いてしまった。。。
第3回目の今回はコーパス収集の方法について説明していきたいと思います。コーパスには青空文庫にある太宰治の小説群を使いますが、特定の作家のみのデータを一括ダウンロードする方法が見つからなかったので、手っ取り早く自前のプログラムでWebスクレイピング(Webから情報を取得すること)をすることにしました。
Webスクレイピングの手順
Webスクレイピングのプログラム実装はコチラの本を参考にしました。
Pythonスクレイピングの基本と実践 データサイエンティストのためのWebデータ収集術(Amazon)
Webから自動で情報を拾ってくるプログラミングをしてみたいという単純な興味から少し前に読んで、サンプルプログラムを作って遊んでいたのですが、まさかこういう使いどころがあるとは。。。むやみにスクレイピングすることは法的に問題もあるそうですが、今回は著作権の切れた小説のダウンロードで、なおかつそれほど負荷もかからないと思うので大丈夫でしょう。
ではWebスクレイピングの手順を説明します。
1.青空文庫の作家ページにアクセス
2.すべての小説のタイトルと図書カードページのリンクを取得
3.小説の図書カードページにアクセス
4.小説のzipファイルをダウンロード
5.すべての小説に対して3,4を繰り返す
下準備
# 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
です。
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'
として、上から順にたどってタグを取得しました。古い仮名遣いが入ってくるといろいろと複雑になりそうなので、今回はコーパスの対象を「新字新仮名」のものに絞ることにしました。大半の小説が「新字新仮名」になっているので、入力データの数には問題ないでしょう。
データベースに保存する際にはtitle
とurl
のほかに、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
は作品のタイトルです。
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()
を定義したので、メインの処理を書きます。
# 「新字新仮名」の作品すべての作品名と作品ページの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〉