他人のはてなブログの読者数を取得するプログラムをpythonで書いておいた
こんにちは、イルカです。
ふと「はてなブログの読者数と読者一覧をプログラムで取得したいな…」と思ったのでpythonで書いてみました。
仕組み
原理としては、まずブログのaboutページに読者と読者リスト(の一部)が載っていることがあるので、それを抜き出します。
iruca21.hateblo.jp
こういうページにある読者の部分ですね。
ポイントは、aboutページをカスタマイズしている人はこのページに読者数、読者リストを載せていない場合があるということです。
しかしここで諦めない。ブログのトップページなどに、必ずその人の読者数だけは載っているはず。
色々と調べてみると、読者数を取得できるAPIを発見しました。
これは使える。
curl -H "X-Requested-With: XMLHttpRequest" \ "http://blog.hatena.ne.jp/api/init?blog=http%3A%2F%2Firuca21.hateblo.jp" {"blog_name":"Iruca Log", "subscribe":false, "private":{}, "blog":"http://iruca21.hateblo.jp", "cookie_received":false, "is_public":true, "can_open_editor":false, "subscribe_url":"http://blog.hatena.ne.jp/iruca21/iruca21.hateblo.jp/subscribe", "quote": {"should_navigate_to_login":true, "star_addable":true, "stockable":true, "supported":true }, "editable":false, "subscribes":"15", "commentable":true, "blog_url":"http://iruca21.hateblo.jp/"}
blog=http%3A%2F%2Firuca21.hateblo.jp の部分をそれぞれのブログのURL(をURLエンコードしたもの)に変えてください。
"subscribes":"15" が読者数みたいですね。
以上の原理を使って、読者数(と読者リストの一部)を取得するコードを書きました。
必要モジュール
pipでrequestsというpythonモジュールを入れておいてください。
yum -y install python-setuptools
easy_install pip
pip install requests
subscription_util.py
はてなブログの読者を取得するためのライブラリです。
subscription_util.py
#!/usr/bin/python #-*- coding:utf-8 -*- import requests import re import json """ はてなIDから、その人のはてなブログの読者数、読者を抜き出すライブラリ """ def fetch_subscriptions( hatena_id ): """はてなIDを入力すると、ユーザのブログのaboutページやトップページのHTMLを取得・解析して ブログの読者数と読者の一部のはてなIDを返却する。 HTMLの解析では読者数が得られなかった場合は、はてなのAPIを利用する。 Args: hatena_id: ユーザのはてなID Returns: ユーザのはてなブログの読者数と、読者(の一部)のはてなIDの配列のタプル。 例: (15, ["hoge", "fuga", "piyo"]) はてなブログをやっていない人の場合は(-1, [])が返却される。 Raises: """ # ブログのURLを取得する try: blog_url = fetch_blog_url( hatena_id ) except KeyError: return (-1, []) # ブログのURLから、ブログのaboutページをとってくる about_page_url = get_about_page_url( blog_url ) # ブログのaboutページに記載してある内容から、 # 読者数と読者リストのタプルを取得 try: subscriptions = get_subscriptions_from_about_page( about_page_url ) except KeyError: # aboutページに読者数を載せていないユーザでも、APIを使えば読者数が取れる subscription_count = get_subscription_count_from_api( blog_url ) subscriptions = ( subscription_count, [] ) return subscriptions def fetch_blog_url( hatena_id ): """はてなIDを入力すると、そのユーザのブログのURLを取得する。 http://blog.hatena.ne.jp/${hatena_id}/ にアクセスすると、 301 Moved Permanently というステータスコードとともに LocationヘッダにブログのURLを入れて返してくれる機能を利用している Args: hatena_id: ユーザのはてなID文字列 Returns: ユーザのメインブログのURL文字列 Raises: KeyError: hatena_idが無効、ブログをやっていないユーザだったなどの理由でブログURLが取得できなかった """ forward_page_url = "http://blog.hatena.ne.jp/"+ str(hatena_id) +"/" response = requests.get( forward_page_url, allow_redirects=False) if response.status_code != 301: raise KeyError("cannot fetch any urls from "+ forward_page_url ) # LocationヘッダにブログのURLが入っている url = response.headers["Location"] # はてなブログをやっていない人だった場合、ブログのURLではなくプロフィールのURLに飛ばされる if "profile.hatena.ne.jp" in url: raise KeyError("this user is not using the blog service. hatena_id="+ str(hatena_id) +", forwarded_page_url="+ str(forward_page_url) ) return url def get_about_page_url( blog_page_url ): """はてなブログのURLから、そのブログのaboutページのURLを取得する。 """ return blog_page_url +"/about" def get_subscriptions_from_about_page( about_page_url ): """ はてなブログのaboutページのURLから、該当ブログの読者数と読者のhatena_idを分かる限り抜きだす。 Args: about_page_url: はてなブログのaboutページのURL文字列 Returns: ブログの読者数と、読者のはてなIDの配列からなるタプル。 (読者数の数値, [読者のはてなID文字列の配列]) 例. (15, [] 読者リストは読者の一部しか表しておらず、読者全員ではないことに注意。 Raises: IOError: はてなブログのaboutページがHTTPで取得できなかった KeyError: はてなブログのaboutページから、読者数や読者のIDをパースできなかった """ response = requests.get( about_page_url ) if response.status_code != 200: raise IOError("cannot get the html of the about page. url="+ str(about_page_url) ) # splitで強引に読者数のspanタグ要素を抜き出す try: subscription_count = int( response.content.split('<span class="about-subscription-count">')[1].split("</span>")[0].strip().split(" 人")[0] ) except: raise KeyError("cannot get the number of subscribers from the about page. url="+ str(about_page_url) ) # splitで強引に読者一覧が含まれるHTMLの部分を抜き出す try: html_containing_subscribers = response.content.split("<div class=\"info\">")[1].split("</div>")[0] except: raise KeyError("cannot get the hatena ids of subscribers from the about page. url="+ str(about_page_url) ) # 上記HTMLには # <a href="http://blog.hatena.ne.jp/rico_note/" class="subscriber" rel="nofollow"><img src="https://cdn1.www.st-hatena.com/users/ri/rico_note/profile.gif" width="16" height="16" alt="rico_note" title="rico_note" class="profile-icon"></a> # のようなHTMLが含まれるはずなので、その中から "rico_note" などのはてなIDを正規表現で抜きだす。 pattern = "http:\/\/blog.hatena.ne.jp\/([a-zA-Z][0-9a-zA-Z_\-]{2,31})\/" r = re.compile(pattern) matched_objs = r.findall( html_containing_subscribers ) subscribers_list = [] # 見つかった読者のはてなID文字列をリストに詰め込んで返却 for matched_obj in matched_objs: subscribers_list.append( matched_obj ) return ( subscription_count, subscribers_list ) def get_subscription_count_from_api( blog_url ): """ 与えられたページのurlをはてなのAPIに与えることで読者数を得る。 curl -v -H "X-Requested-With: XMLHttpRequest" "http://blog.hatena.ne.jp/api/init?name=&blog=http%3A%2F%2Fgwgw.hatenablog.com%2Fabout" Args: url: はてなブログ内のページ(ブログのトップページなど) Raises: KeyError: 与えられたページのURLから、読者数を取得できなかった """ target_url = "http://blog.hatena.ne.jp/api/init?name=&blog="+ blog_url response = requests.get(target_url, headers={"X-Requested-With": "XMLHttpRequest"} ) # 読者数をjson responseから抜き出す try: subscription_count = int( json.loads( response.content )["subscribes"] ) except: raise KeyError("cannot get the number of subscribers from the page. url="+ str( target_url ) ) return subscription_count
実行してみる
上記のsubscription_util.pyと同じディレクトリから、実行してみます。
実行するためのスクリプト
[root hatena]# python Python 2.7.12 (default, Sep 1 2016, 22:14:00) [GCC 4.8.3 20140911 (Red Hat 4.8.3-9)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import subscription_util >>> print subscription_util.fetch_subscriptions("p_shirokuma") (2047, ['neojin', 'spencer07', 'uza_momo', 'hdtyamada', 'nomatterxxx', 'ak1aims', 'suchi', 'wataru45', 'cat-whisker', 'beta-opinion', 'tyunnomidesu', 'kuripton', 'boku0', 'esu08052', 'haretokidoki86596355', 'botan0915', 'tofudot', 'ao-re', 'fummy', 'animisum', 'Znvn3j', 'akyska', 'ozabun', 'maisonnaoko', 'authenticlife', 'family-labo-fukuyama', 'taketack', 'high_grade_works', 'kojimat', 'mitaniya77', 'miunenearu', 'aqua935', 'hemispherwhistle', 'saachan0000', 'l_grgr_l', 'oshaberiitboy', 'katohaya2125', 'rankattt', 'nanapi2016', 't-konishi4976', 'azumami', 'glovetoss', 'realize10', 'yamakokun', 'fm315', 'talbotbuy', 'yoshirn75k', 'min117', 'morinonak', 'shizuokershugo', 'zunkororin', 'mogumoguhina', 'shin038', 'vilar5275', 'afugoro', 'mikachanko4281196', 'non-ishi566', 'motosaaaan', 'AobadaiAkira', 'KONOYUBITOMARE', 'krkctisi', 'doll_satomiii', 'sun_lee_rui_key', 'mugendai53', 'katsu-shin', 'yuma_sun', 'lifeofdij', 'IOQO', 'sakuranorihiko', 'inazuma2073', 'namellow', 'karatte', 'Pompomy', 'temiage', 'hiranoglyph', 'shinth1', 'nepiasan', 'siosiotaro', 'schunk', 'goodbey2012', 'mysweetr', 'hiroyuxxx', 'thresholdjp', 'velminton', 'for-happy-life', 'hanaekiryuin', 'naniwasetuyakudou', 'tu-ku-si', 'chihuahua-works', 'minamanami', 'dialmmm', 'umakosan', 'showr7', 'rerittu', 'mashimoc', 'poti1974', 'pfassistant', 'OgasawaraMakoto', 'keira-p', 'mkyk1616'])
うん、動いた動いた。
p_shirokumaさんスゲー、読者2047人。(2017年4月9日現在)