11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【2020年版】青空文庫から本文をスクレイピングして加工する

Posted at

概要

青空文庫に掲載されている作品の本文を Python でスクレイピングしていい感じに加工しました。その際、ところどころハマったのでその覚書。

環境

  • macOS Catalina
  • Anaconda 3系
    • Python 3系
    • Jupyter Notebook
    • BeautifulSoup4

本文の取得

まずは青空文庫から作品の本文を取得します。
やることは基本的にこの記事(https://qiita.com/icy_mountain/items/011c9f56151b9832b54d) に書いてある通りで、[青空文庫 API(https://qiita.com/ksato9700/items/626cc82c007ba8337034) を叩いて本文の HTML をフェッチします。その際、URLの作品ID部分は book_id と変数を置くことにします。
しかし自分の環境だとこの通りにはできませんでした。まず、ターミナル上ではなく Jupyter Notebook 上で操作していたので、!wget コマンドは使えません。
そこで Python で API に GET リクエストを送るコマンドを調べてみると urllib2.urlopen()reqests.req() の二つが出てくるんですが、このうち urllib2 は Python2 用のライブラリなので使えません。Python3 では urllib3 ないし urllib という名前に変わっているようですが、よくわからなかったので requests ライブラリを使うことにしました。つまり GET メソッドによるAPIのフェッチは次のようになります。

import requests
res = requests.get('http://pubserver2.herokuapp.com/api/v0.1/books/{book_id}/content?format=html'.format(book_id))

本文の抽出

次に、フェッチしてきた HTML データを BeautifulSoup4 で使える形式に変換します。

from bs4 import BeautifulSoup
soup = BeautifulSoup(res.text, 'html.parser')

ここから title タグの本文を取得するには以下のように書きます。

soup.find('title').get_text()
# -> 夢野久作 青水仙、赤水仙

タイトルはいい感じに半角スペースで区切られているので、ここについて split() をすれば著者名とタイトルに分けられます。
外国人著者の場合など、うまくいっていない場合もあるので、いちおう残りの情報の有無も変数に置いておきます。

title_list = title.split(' ')
        book_author = title_list[0] # 著者名
        book_title = title_list[1] # タイトル
        book_title_info = len(title_list) > 2 # タイトルが途切れているか

一方、(本来の意味での)本文は main_text クラスの div タグなので、以下のようになるでしょう。

soup.find('div', {'class': 'main_text'}).get_text()
# -> \n\n\n\r\n\u3000うた子さんは友達に教わって、水仙の根を切り割って、赤い絵の具と青い絵の具を入れて、お庭の隅に埋めておきました。[...]

今回は句点ごとに文章を区切りたいので、以下のように一文ごとのリスト形式にしておきます。
冒頭の一文が欲しいのであれば、 0 番目の要素として取得できます。

text_list = soup.find('div', {'class': 'main_text'}).get_text().split('')
text_first = text_list[0] + "" # 冒頭の一文

本文の精製

このままだと本文が汚いので、不要な要素を削除して本文を精製していきます。
まず、改行コード \n や半角スペース \u3000 に当たるコードが混じってしまっているので、 get_text() 後に strip() で落とします。

text_list = soup.find('div', {'class': 'main_text'}).get_text().strip('\r''\n''\u3000').split('')

本文中に含まれるカッコで囲まれたルビの部分を削除するために、以下のコードを変換直後に追加してルビを削除します。

    for tag in soup.find_all(["rt", "rp"]):
        tag.decompose() # タグとその内容の削除

たまに句点で区切られず、冒頭の一文が延々と続く場合があるので、あまりに長ければ別の文字列に置き換えることにします。長さの基準 100 は適当です。

text_first = text_list[0] + "" if (len(text_list[0]) < 100) else "too long" # 冒頭

最後に、以上の工程を関数化するにあたり、相当する作品 ID が存在せずフェッチに失敗していると抽出の際に NoneType エラーとなるので、タグとクラスの有無でこれを除外しておきます(もうちょっとマシな書き方があると思うのですが)。

if (soup.find('title') is None) or (soup.find('div') is None) or (soup.find('div', {'class': 'main_text'}) is None):
        return [book_id, '', '', '', '', '' ]
else:
        title =  soup.find('title').get_text()
        [...]
bookInfo.py
def bookInfo(book_id):
    import requests
    from bs4 import BeautifulSoup

    res = requests.get(f'http://pubserver2.herokuapp.com/api/v0.1/books/{book_id}/content?format=html')
    
    soup = BeautifulSoup(res.text, 'html.parser')
    for tag in soup.find_all(["rt", "rp"]):
        tag.decompose() # タグとその内容の削除
    
    if (soup.find('title') is None) or (soup.find('div') is None) or (soup.find('div', {'class': 'main_text'}) is None):
        return [book_id, '', '', '', '']
    else:
        title =  soup.find('title').get_text()
        title_list = title.split(' ')
        book_author = title_list[0] # 著者名
        book_title = title_list[1] # タイトル
        book_title_info = len(title_list) > 2 # タイトルが途切れているか
        
        print(soup.find('div', {'class': 'main_text'}))
        text_list = soup.find('div', {'class': 'main_text'}).get_text().strip('\r''\n''\u3000').split('')
        text_first = text_list[0] + "" if (len(text_list[0]) < 100) else "too long" # 冒頭
        else:
            text_last = ""
    
        list = [book_id, book_author, book_title, book_title_info, text_first]
        print(list)
        return list
bookInfo(930)
# -> [930,
# '夢野久作',
# '押絵の奇蹟',
# False,
# '看護婦さんの眠っております隙を見ましては、拙ない女文字を走らせるので御座いますから、さぞかしお読みづらい、おわかりにくい事ばかりと存じますが、取り急ぎますままに幾重にもおゆるし下さいませ。']

精製データの CSV 出力

上記の関数を使って得られたリストを順に追加して二次元リストを作り、それを CSV ファイルとして出力したいと思います。
まず csv をインポートし、ファイルをオープンします。
ちなみにファイルがすでに存在し、全文上書きではなく後ろに書き足すときは 'w''a' に変えます。

import csv
f = open('output.csv', 'w')
writer = csv.writer(f, lineterminator='\n')

空のリストを作成し、 for ループを回して関数を実行した返り値をリストに追加していきます。
ちなみに青空文庫 API では一つの作品からの取得に数秒かかりました。あんまり大量にリクエストしすぎると利用不能になってしまう気がするので10とか100とか小さな単位ずつ実行するのがいいと思います。

csvlist = []
for i in range(930, 940):
    csvlist.append(bookInfo(i))

最後にファイルを閉じて完了です。

writer.writerows(csvlist)
f.close()

CSV ファイルが出力されました。 Google スプレッドシートなどに読み込ませると便利です。

930,夢野久作,押絵の奇蹟,False,看護婦さんの眠っております隙を見ましては、拙ない女文字を走らせるので御座いますから、さぞかしお読みづらい、おわかりにくい事ばかりと存じますが、取り急ぎますままに幾重にもおゆるし下さいませ。

余談

  • 元々最初から Google スプレッドシートに GAS の関数で取得した値を直接書き込みたかったのですが、青空文庫 API で取得した HTML は meta タグが壊れているようで XML 構文解析に失敗するので、いろいろ試したんですがうまくいきませんでした。それで Python で一旦 CSV に変換するという手間を取ること。
  • データがうまく取得できたとき「いいダシが取れたな〜」とつい思ったんですが、 BeautiflSoup ってつまり日本語で言う「ダシ」であることに気づいてしまった…。
11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?