はてなブログの読者をクロールしてブログ読者のデータを集めるクローラを書いた[python]
こんにちは、イルカです。
今回は、はてなブログ読者をクロール(巡回)して、各ブログの読者数を調べたりはてなユーザのソーシャルネットワークを調べたりするプログラムを書いてみました。
はじめに
こちらの記事で、はてなブログのページから読者数を抜き出すプログラムについて触れています。
iruca21.hateblo.jp
こちらで作ったsubscription_util.pyというプログラムを利用して、今回のクローラを書いています。
また、取得したブログの読者数はローカルのSQLiteに保存します。
データベースの作成
さて、まずはweb上から取得してきたデータを保存するSQLiteのテーブルを定義します。
こんな感じにしてみました。
データベース名: hateblo_subscription.db
テーブル名: subscription
列名 | データ型 | インデックス | メモ |
---|---|---|---|
hatena_id | text | 主キー | ユーザのはてなID文字列 |
subscription_count | integer | ユーザのメインのはてなブログの読者数 | |
update_date | text | クローラがデータを更新した最終日時 |
下記のpythonスクリプトを使って、テーブルを生成しておきます。
create_hatena_db_and_table.py
#!/usr/bin/python #-*- coding:utf-8 -*- # python2.7標準で入ってるはず import sqlite3 db_name = "hateblo_subscription.db" table_name = "subscription" sql = """CREATE TABLE %s( hatena_id text, subscription_count integer, update_date text, PRIMARY KEY(hatena_id)) """ % ( table_name ) print sql #ローカルのDBへの接続取得 connection = sqlite3.connect(db_name) #カーソル取得 cursor = connection.cursor() #SQL実行 cursor.execute( sql ) #コミット connection.commit() #接続を閉じて終了 connection.close()
SQLiteの基本の使い方についてはこちらの記事もどうぞ。
iruca21.hateblo.jp
データベースへのアクセッサの作成
データベースの仕様を決めたので、次は作ったテーブルにアクセスするモジュールを書く必要があります。
SubscriptionDataAccessorという名前に決めて、データを保存する役目を担うクラスを作ります。
subscription_data_accessor.py
#!/usr/bin/python #-*- coding:utf-8 -*- class SubscripitonDataAccessor: """ ローカルにSQLiteに保存しているSubscription(購読者数) データへの操作を行うクラス """ DB_NAME = "hateblo_subscription.db" TABLE_NAME = "subscription" # データの存在確認のためのSQL EXISTENCE_CHECK_SQL = """ SELECT hatena_id FROM %s WHERE hatena_id = "%s" """ # 特定の日付以降に更新されたデータ存在確認のためのSQL EXISTENCE_CHECK_WITH_DATE_SQL = """ SELECT hatena_id FROM %s WHERE hatena_id = "%s" AND update_date >= "%s" """ # 購読者数データ上書き更新用SQL REPLACE_SQL = """ REPLACE INTO %s VALUES( "%s", %d, "%s" ) """ def __init__(self): """クラスの初期化と同時に、 SQLiteのDBへの接続も取得する """ import sqlite3 self.connection = sqlite3.connect( self.DB_NAME ) def data_exists( self, hatena_id, hour=0 ): """既にDBの中に該当ユーザの情報が入っているかどうかをチェックする。 既に入っていたらtrueを返す。 Args: connection: ローカルのsqliteのDBへのコネクションオブジェクト hatena_id: ユーザのはてなID文字列 hour: データの更新日時が何時間以内のデータを存在するとみなすか。 0だった場合は更新日時のチェックはしない。 Returns: もし既に入っていたらtrue, そうでなければfalse """ cursor = self.connection.cursor() if int(hour) == 0: sql = self.EXISTENCE_CHECK_SQL % ( self.TABLE_NAME, hatena_id ) else: # hour引数を考慮 import datetime update_date_begin = datetime.datetime.now() - datetime.timedelta(hours=int(hour)) sql = self.EXISTENCE_CHECK_WITH_DATE_SQL % ( self.TABLE_NAME, hatena_id, update_date_begin.strftime( '%Y-%m-%d %H:%M:%S') ) #print sql # SQL実行 cursor.execute( sql ) result = cursor.fetchall() # 1行でもデータが入ってればtrue for row in result: return True # そうでなければfalse return False def replace( self, hatena_id, subscription_count ): """ 購読者数のデータを現在時刻で上書き更新する。 Args: hatena_id: データを更新するユーザのはてなID文字列 subscription_count: ブログ購読者数の数値 Returns: なし """ import datetime sql = self.REPLACE_SQL % (self.TABLE_NAME, hatena_id, subscription_count, datetime.datetime.now().strftime( '%Y-%m-%d %H:%M:%S') ) cursor = self.connection.cursor() #print sql # SQL実行 cursor.execute( sql ) def commit( self ): """ ローカルのSQLiteデータベースにcommitする""" self.connection.commit() def close( self ): """ ローカルのSQLiteデータベースへの接続を閉じる""" self.connection.close() # 簡単のため、このクラスを単体で実行したときに # 動作確認を行う if __name__ == "__main__": subs_accessor = SubscripitonDataAccessor() subs_accessor.replace("iruca21", 15) subs_accessor.commit() print subs_accessor.data_exists("iruca21") print subs_accessor.data_exists("iruca21", 24) subs_accessor.close()
これでクローラを書く準備が整いました。
クローラ(巡回モジュール)の作成
さて、やっとメインのクローラを作ります。
仕組みは単純。こちらの記事で紹介しているモジュールが、
- 特定ユーザのはてなブログの読者数
- 特定ユーザのはてなブログの読者のリスト(一部)
を返してくれるので、この読者数をDBに保存しながら、読者のそれぞれをまたさらに調べていきます。
Aさんのブログの読者のBさんのブログの読者のそのまた読者の…
と調べていくということですね。
スタックオーバーフローを避けるために、最初にクロールを開始したユーザから数えて10人以上読者をまたいだユーザは調べないとしています。
また、はてなさんのサーバに迷惑をかけないように5秒に1回しかデータを取りに行かないようにクロール速度を調整しています。
subscription_crawler.py
#!/usr/bin/python #-*- coding:utf-8 -*- import time from subscription_data_accessor import SubscripitonDataAccessor import subscription_util class SubscriptionCrawler: """はてなブログの読者を再帰的に巡回して、 読者数を集めて回るクローラー。 サーバへのDoS攻撃にならないように、クロール速度に注意している。 """ # 1ユーザを巡回するたびにスリープする時間 CRAWL_INTERVAL = 5 # 何時間前のデータまで最新のデータとみなすか NEED_UPDATE_HOUR = 24 # 最初のユーザから何層ブログ読者をネストしたところまで最大で調べるか MAX_NEST_DEPTH = 10 # すでに巡回したユーザ数 user_count = 0 def __init__(self, initial_hatena_id): """コンストラクタ。 Args: initial_hatena_id: 巡回を始める最初のユーザ """ self.initial_hatena_id = initial_hatena_id self.data_accessor = SubscripitonDataAccessor() def crawl(self, hatena_id="", nest_count=0): """再帰的に巡回してブログ読者数を集める。 Args: hatena_id: 巡回するユーザのはてなID文字列. 指定されなかった場合はinitial_userを巡回する nest_count: initial_userから数えて、読者を何層ネストしたユーザを調べているかを表わす数値 Returns: なし """ if hatena_id == "": hatena_id = self.initial_hatena_id # 最新のデータが入っていれば改めて巡回はしない if self.data_accessor.data_exists( hatena_id, hour=self.NEED_UPDATE_HOUR ): return print "fetching %s 's subscription... (user_count=%d, nest_count=%d)" % ( str(hatena_id), self.user_count, nest_count ) time.sleep(self.CRAWL_INTERVAL) # DoSにならないようにSleepを入れる try: # スクレイピングしてくる subscription = subscription_util.fetch_subscriptions( hatena_id ) subscription_count = subscription[0] subscribers = subscription[1] except: # 読者数が取得できない人は読者数-1人としてデータ挿入 print "couldn't get %s 's subscription" % str(hatena_id) subscription_count = -1 subscribers = [] # データ挿入 self.data_accessor.replace( hatena_id, subscription_count ) self.user_count += 1 # 10人分データを保存するごとにcommitを行う if self.user_count % 10 == 0: print "commit!" self.data_accessor.commit() # そのユーザのブログ読者に対して再帰的に巡回を行う. # ただし、スタックオーバーフローを避けるために最大のネスト数を設けておく if nest_count < self.MAX_NEST_DEPTH : for subscriber_id in subscribers: self.crawl( subscriber_id, nest_count + 1 ) return # 簡単のため、このスクリプト自体を実行したときに # 動作確認を行う。 if __name__ == "__main__": initial_user = "iruca21" crawler = SubscriptionCrawler("iruca21") # 巡回開始 crawler.crawl()
さあ、これで全てのモジュールがそろいました。
実行してみた
さっそく実行してみましょう。
[root@hoge hatena]# python subscription_crawler.py fetching iruca21 's subscription... (user_count=0, nest_count=0) fetching hiro-loglog 's subscription... (user_count=1, nest_count=1) fetching hourou_world 's subscription... (user_count=2, nest_count=2) fetching tk4876 's subscription... (user_count=3, nest_count=3) fetching nicenuts 's subscription... (user_count=4, nest_count=4) fetching mochi36 's subscription... (user_count=5, nest_count=5) fetching yasaiitame07 's subscription... (user_count=6, nest_count=6) fetching seiyablog 's subscription... (user_count=7, nest_count=7) fetching kirarin010914 's subscription... (user_count=8, nest_count=8) couldn't get kirarin010914 's subscription fetching shinshiraoka1411 's subscription... (user_count=9, nest_count=8) commit! fetching gbh06101 's subscription... (user_count=10, nest_count=9) (以下略)
うんうん、ちゃんとユーザを巡回してるな。
データベースの中にちゃんとデータが入ってるかも見てみましょう。
[root@hoge hatena]# sqlite3 hateblo_subscription.db SQLite version 3.7.17 2013-05-20 00:56:22 Enter ".help" for instructions Enter SQL statements terminated with a ";" sqlite> select * from subscription; iruca21|16|2017-04-11 20:20:19 hiro-loglog|151|2017-04-11 20:20:24 hourou_world|69|2017-04-11 20:20:29 tk4876|80|2017-04-11 20:20:35 nicenuts|70|2017-04-11 20:20:40 mochi36|39|2017-04-11 20:20:46 yasaiitame07|7|2017-04-11 20:20:51 seiyablog|41|2017-04-11 20:20:56 kirarin010914|-1|2017-04-11 20:21:01 shinshiraoka1411|56|2017-04-11 20:21:07
よしよし、データが溜まっていってるな。
満足。
考察
このデータを貯めれば、はてなブログの読者関係が作るソーシャルネットワークを分析することができます。
じっとデータを眺めていると、色々面白いことが見えてきます。
「あれ、無名なユーザばかりが読者になってるブログがある…」
みたいな不気味なユーザも居ます。
つまり、この分析によって
「不正にはてなユーザを大量作成して、読者を増やしているブロガーを検知できる」
というような可能性もあります。
実際にそういう怪しいブログを洗い出したり、他にも色々と面白いことを分析するのはまた別記事で。
(はてなさん、僕のプログラム買いとってくれたりしないかな笑)
ではでは!