roombaの日記

読書・非線形科学・プログラミング・アート・etc...

本の季節感を可視化してみる【D3.js】【Pythonによるスクレイピング】

目次

はじめに

以前の記事で、読書メーターという読書記録サービスからデータを収集して分析するということをやってみました。roomba.hatenablog.com

この記事では、その応用として「本の季節感を可視化」してみます。より具体的には、Pythonによる「読書メーター」のスクレイピングによって「ある本が何月に読まれることが多いか」を調べ、D3.jsというデータ可視化Javascriptライブラリを用いてブラウザ上にグラフを描画します。

これにより、『クリスマス・キャロル』をクリスマスに読む人は多いのか、『桜の森の満開の下』はやっぱり4月に読む人が多いのか、といったことが分かるようになります。つまり「本の季節感を可視化」できるわけですね。

f:id:roomba:20151121154234p:plain

読書メータースクレイピング

概要

読書メーターのリンクを以下に貼り付けておきます。
読書メーター - あなたの読書量をグラフで記録・管理

読書メーターでは、本を読み終わったら「読んだ本」として登録し、感想を記したり他のユーザの感想を読んだりすることができるようになっています。
ある特定の本のページを開くと、各ユーザが「読んだ本」に登録した日付を感想とともに見ることができるため、その日付データを集めていきましょう。

Pythonによる実装

実装上のポイント

読書メーター内の本のページは以下のようなURLとなっています。これはログインしなくても開くことができます。

http://bookmeter.com/b/【本のID】

ここで、【本のID】はどうやらISBN-10になっているみたいです。

このURLにアクセスし、HTMLをのぞいてみると、以下のように各ユーザの読了日が記されています。この場合は2015年8月2日に読み終えたユーザーがいるということです。

<span class="value-title" title="2015-08-02"></span>

したがって、PythonのHTMLParserによってこの部分を取り出していけばOKです。

なお、その本を読んだユーザが多い場合、複数ページにまたがって上記の解析を行う必要があります。例えば2ページ目のURLは以下のようになっているので、「次のページ」がなくなるまで順番に解析していきます。

http://bookmeter.com/bl/【本のID】?p=2
ソースコード

以下のように始めます。HTML解析のためのHTMLParserと、読書メーターにアクセスしてHTMLを取得するためのurllib2をimportする必要があります。

#! /usr/bin/python                                                                                                                                                       
# -*- coding: utf-8 -*-                                                                                                                                                  

from HTMLParser import HTMLParser
import urllib2

HTMLの解析は以下の関数によって行います。

# ある本のHTMLを受け取って、読了日のリストを返す関数                                                                                                                     
# 引数:HTMLのstring, 返り値:読了した日付(str)のリスト                                                                                                                  
def parse_book(book_html):
    date_list = []# ここに収める                                                                                                                                         
    class WordParser(HTMLParser):# HTMLパーサ                                                                                                                            
        def __init__(self):
            HTMLParser.__init__(self)
        def handle_starttag(self, tag, attrs):
            if tag == 'span':
                if len(attrs) == 2 and len(attrs[0]) == 2:
                    if attrs[0][0] == "class" and attrs[0][1] == "value-title":
                        if attrs[1][1][0] == "2":
                            date_list.append(attrs[1][1])
    wp = WordParser()
    wp.feed(book_html)
    wp.close()
    return date_list

やっていることは、与えられたhtmlから以下のような日付を見つけてゆき、そのリストを返すという感じです。

<span class="value-title" title="2015-08-02"></span>

本のIDをもとに読書メーターにアクセスし、読まれた日付のリストを返す関数が以下のget_date()です。先ほどのparse_book関数を用いています。

# ある本のIDを受け取って、その本が読まれた日付のリストを返す関数                                                                                                         
# 引数:本のID(str), 返り値:日付(str)のリスト                                                                                                                           
def get_date(book_id):
    date_list = [] # ここに日付を収める                                                                                                                                  
    count = 1 # その本の感想を表示するページが複数に渡る場合、その数                                                                                                     

    # HTMLを取得                                                                                                                                                         
    book_url = "http://bookmeter.com/b/" + book_id
    response = urllib2.urlopen(book_url)
    book_html = unicode(response.read(), 'utf-8')

    date_list +=  parse_book(book_html)# 読了日リストを取得                                                                                                              

    # 感想が多数の場合、複数のページに渡ってHTMLを取得・解析する                                                                                                         
    while u">次へ" in book_html:
        # なぜか2ページ目以降は/b/が/bl/になる                                                                                                                           
        book_url = "http://bookmeter.com/bl/" + book_id + "?p=" + str(count+1)
        response = urllib2.urlopen(book_url)
        book_html = unicode(response.read(), 'utf-8')

        date_list += parse_book(book_html)# date_listに追加                                                                                                              
        count += 1

    return date_list

さいごに、main関数は以下のようにします。sample_idのコメントアウトを変えることで対象とする本を変えることができます。もちろんこれ以外のsample_idでも大丈夫です。

# Main関数                                                                                                                                                               
if __name__ == '__main__':
    sample_id = str(4102030093) # クリスマス・キャロルのID(サンプル)                                                                                                     
#    sample_id = str(4101010161) # 二百十日・野分                                                                                                                        
#    sample_id = str(4101010099) # 草枕                                                                                                                                                                                                                                                      
#    sample_id = str(4061960423) # 桜の森の満開の下                                                                                                                      
#    sample_id = str(4101001014) # 雪国                                                                                                                                  

    # (1) 本のIDから日付のリストを取得                                                                                                                                   
    date_list = get_date(sample_id)

    # (2) 月別読了数をcsvファイルに書き出す                                                                                                                                                                                                                                                      
    f = open("date_hist" + sample_id + ".csv", "w")
    f.write("month,number" + "\n")
    for i in range(12):
        f2.write(str(i+1) + "," +
                 str(len(filter(lambda x: int(x[1]) == i+1, [x.split('-') for x in date_list]))) + "\n")
    f.close()

結果

以下のようなcsvファイル(date_hist4102030093.csv)が得られました。

month,number
1,15
2,6
3,6
4,2
5,3
6,3
7,7
8,7
9,7
10,7
11,15
12,74

これを使って、以下ではグラフを描画していきます。

D3.jsによるグラフの描画

概要

Pythonプログラムによって得られたcsvファイルを元に、円グラフを作成します。ここではD3.jsというライブラリを用いました。

実装

ほとんど以下のサンプルのまんまです。
Donut Chart

csvファイルはWeb上にアップロードしておき、プログラム中ではそのURLを記します。例えば
http://jsrun.it/assets/C/i/T/d/CiTdz
などです。

ソースコードは以下で見ることができます。
本の季節感を可視化する - jsdo.it - Share JavaScript, HTML5 and CSS

結果

以下のように、いくつかの本について円グラフを描画することができました。
http://jsrun.it/roomba/sdMt
順番に見ていきます。

クリスマス・キャロル(新潮文庫)

やはりというか、圧倒的に12月に読む人が多いですね。
f:id:roomba:20151121162016p:plain

二百十日・野分 (新潮文庫)

二百十日というのは9/1ごろをさします。9月に読んでいる人が若干多めですね。
f:id:roomba:20151121162040p:plain

桜の森の満開の下 (講談社文芸文庫)

春に読んでいるひとが多いです。
f:id:roomba:20151121154234p:plain

雪国 (新潮文庫)

冬に読む人が多めに見えます。有意な差かといわれると微妙ですが。
f:id:roomba:20151121162137p:plain

草枕 (新潮文庫)

名作です。そんなに季節感の強い作品ではないので、まんべんなく散らばっていますね。
f:id:roomba:20151121162202p:plain

おわりに

こんな感じの分析を沢山の本に対して行えば、「12月にオススメの本」みたいな機能が実現できると思います。読書メーターさんがそういう機能を実装してくれないかな…。

プログラムは自由に使用してください。今回のスクレイピング(って言うんですよね?)ではほとんど負荷をかけないと思いますが、アレンジしたプログラムを書いてみるときには短時間に大量のアクセスをしないような配慮が必要です。

ちょっとプログラムの説明が雑になっている気がするので、気軽にコメントなどでご質問ください。