はてなブログのスターの数とブックマークの数をカウントして CSV ファイルに出力するアプリをアプリを作成しました。
はてなブログ AtomPub、はてなスター取得API、はてなブックマーク件数取得API を使用しています。
機能的には次の特徴があります。
◎自分のはてなブログのURLを指定して各記事のスターの数とブックマークの数を出力
◎スターの数は色ごとに出力
◎結果は CSV ファイルに出力
◎出力する記事の数を指定可能
各 API の使い方をサンプルコードも交えて説明します。
📄『はてなブログのスターとブックマークの数を取得するアプリ【フリー】🔗』
目次
- ◆機能・特長
- ◆考え方
- ◆はてなブログ - AtomPub
- ◆はてなスター
- ◆はてなブックマーク
- ◆アイキャッチ画像のURLの取得
- ◆CSV出力
- ◆全体処理
- ◆必要なパッケージ
- ◆全体のソース
- ◆さいごに
- ◆参考
◆機能・特長
◆考え方
基本的な考え方です。
- はてなブログ AtomPub を使用してブログのコレクションを取得する
- 取得したコレクションから記事の URL を取得する
認証には、WSSE を使用しました。
◆はてなブログ - AtomPub
はてなブログの記事の一覧を取得するために、はてなブログ AtomPub を使用します。
Atom Publishing Protocol とは
Atom Publishing Protocol(以下 AtomPub) はウェブリソースを公開、編集するためのアプリケーション・プロトコル仕様です。はてなブログのAtomPubと通じて、開発者ははてなブログのエントリを参照、投稿、編集、削除するようなオリジナルのアプリケーションを作成できます。 Hatena Developer Center
◇用語
はてなブログ AtomPub を使う上で出てくる用語を説明します。
- コレクション:記事の集合(トップページの情報に近い)
- メンバ:個々の記事➡多分コレクションでblog-idを取得してアクセスするみたい(未確認)
- サービス文書:コレクションの一覧➡今は一つしか返らない
- カテゴリ文書:コレクションで使用されるカテゴリを記述する文書
◇URIと操作
はてなブログによると、 URI を URI Template の記法に基づいて以下のように表記しているようです。
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/{entry_id}
各変数の意味と書式は次のようになります。
はてなブログProをお使いで独自ドメインをご利用の方は次のような注意事項があります。
はてなブログProの独自ドメイン機能をご利用の方は、独自ドメイン設定前のブログのドメインがブログのIDとなります。ブログの詳細設定のAtomPub項内のルートエンドポイントをご参照ください。 Hatena Developer Center
- コレクションURI:
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry
- メンバURI:
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/{entry_id}
- サービス文書URI:
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom
どのようなコレクションが存在するかを返すサービス - カテゴリ文書URI:
https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/category
コレクションで使用されるカテゴリを返す
API がサポートしている操作
ブログの操作 (コレクション)
ブログエントリ(記事)の操作 (メンバ)
サービスの操作 (サービス文書)
- コレクション一覧の取得 (サービス文書URI への GET)
カテゴリの操作 (カテゴリ文書)
- カテゴリ一覧の取得 (カテゴリ文書URI への GET)
◇WSSE認証
📄はてなフォトライフへ画像をアップロードするアプリの作り方(AtomAPIの使い方)【Python】🔗
◇コレクションURI - ブログエントリの一覧取得
コレクション URI を GET することで、ブログエントリ一覧を取得できます。
- 一度に複数のブログエントリを取得 ➡ トップページの表示数に依存
- 次ページがある場合、次ページを示す要素が返る
要素は属性 rel=next となる link タグで href 属性が次ページの URI となります。 - 次ページのブログエントリを取得するには、
page
パラメータを URI に付与する
【リクエスト方法】
- トップページ
GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry - 次ページ以降
GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1234567890
◇コレクションURIの応答
Pythonの場合、requestsパーケージの get() メソッドで要求を出し、戻り値(Responseオブジェクト)の text 属性に XML 文書が返ってきます。
コレクションの戻りXMLのタグの概要
link rel="next"
:href 属性が次ページのURL、これをたどってすべての記事をたどるentry
:記事の始まり、これが複数記事ごとにあるlink rel="alternate"
:herf 属性が記事のURLtitle
:タイトルupdated
:更新日(詳細は不明) 例2022-05-20T17:36:11+09:00published
:投稿日 例 同上category
:term 属性にカテゴリ名、複数ありsummary
:記事のサマリーcontent
:記事の内容hatena:formatted-content
:フォーマットされた記事の内容
▽XML のサンプル
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app"> <link rel="first" href="https://blog.hatena.ne.jp/{はてなID}}/{ブログID}/atom/entry" /> <link rel="next" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1377584217" /> <title>ブログタイトル</title> <link rel="alternate" href="http://{ブログID}/"/> <updated>2013-08-27T15:17:06+09:00</updated> <author> <name>{はてなID}</name> </author> <generator uri="http://blog.hatena.ne.jp/" version="100000000">Hatena::Blog</generator> <id>hatenablog://blog/2000000000000</id> <entry> <id>tag:blog.hatena.ne.jp,2013:blog-{はてなID}-20000000000000-3000000000000000</id> <link rel="edit" href="https://blog.hatena.ne.jp/{はてなID}/ ブログID}/atom/edit/2500000000"/> <link rel="alternate" type="text/html" href="http://{ブログID}/entry/2013/09/02/112823"/> <author><name>{はてなID}</name></author> <title>記事タイトル</title> <updated>2013-09-02T11:28:23+09:00</updated> <published>2013-09-02T11:28:23+09:00</published> <app:edited>2013-09-02T11:28:23+09:00</app:edited> <summary type="text"> 記事本文 リスト1 リスト2 内容 </summary> <content type="text/x-hatena-syntax"> ** 記事本文 - リスト1 - リスト2 内容 </content> <hatena:formatted-content type="text/html" xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#"> <div class="section"> <h4>記事本文</h4> <ul> <li>リスト1</li> <li>リスト2</li> </ul><p>内容</p> </div> </hatena:formatted-content> <app:control> <app:draft>no</app:draft> </app:control> </entry> <entry> ... </entry> ... </feed>
◇Python で GET リクエスト
headers, endpoint を指定して requests の get メソッドで電文を送ります。
import requests
が必要です。
▶endpoint は、
コレクションURI - ブログエントリの一覧取得で示した通りトップページと次ページ以降の場合を扱えるように引数で受け取るようにします。
▶headers は、
WSSE 認証用の X-WSSE ヘッダを作成し、指定します。
▶get() メソッドの結果は戻り値で取得できます。 戻り値の text 属性は、XML です。
▶エラー処理
get() メソッド実行後に raise_for_status() メソッドを実行すると status_code が200番代以外の時に例外が発生します。
◎コード
▽GETリクエスト
def get_hatena(self, next_url:str = None) -> str: """ はてなブログの記事取得 Args: str: None:ブログのトップ画面を取得する場合 url:前回の取得で得られた次のページのurl Returns: str: xml """ blog_id = settings_hatena_url.blog_id headers = {'X-WSSE': self.wsse(os.getenv("py_hatena_username"), os.getenv("py_hatena_api_key"))} if next_url: endpoint = next_url else: endpoint = f'https://blog.hatena.ne.jp/{os.getenv("py_hatena_username")}/{blog_id}/atom/entry' # コレクション # endpoint = f'https://blog.hatena.ne.jp/{os.getenv("py_hatena_username")}/{blog_id}/atom' # サービス文書 try: r = requests.get(endpoint, headers=headers) print(f'--request result-- status code={r.status_code}') r.raise_for_status() # 200番代以外は例外を発生 # 例外がないのでxmlを返す result = r.text except requests.exceptions.ConnectionError: sys.stderr.write('Connection Error!') result = "" except: sys.stderr.write(f'Error!\nstatus_code: {r.status_code}\nmessage: {r.text}') result = "" return result
◇Python で XML 解析
ElementTree XML API を使用して XML を解析します。
import xml.etree.ElementTree as ET
が必要です。
コレクションURIの応答で示した XML を解析して、目的のデータを取得します。
link タグは属性を見て解析する必要があるのでXPathを指定して解析します。
その他のタグは直接解析できるのでタグ名を指定して解析します。
◎デフォルト名前空間の指定方法
コレクションURIが返す XML は名前空間が指定されています。
名前空間の中でも接頭語のない(デフォルト)名前空間が使用されています。
ElementTree で XML を解析する場合、find(), findall(), iter() メソッドを使用しますが、使い方の違いをまとめてみました。
メソッド | 検索対象 | 名前空間の指定 | 検索範囲 | 戻り値(or None) |
---|---|---|---|---|
iter | タグ | 直接 | 子孫 | エレメントのイテレータ |
find | タグ,パス | 辞書 | 子 | エレメント |
findall | タグ,パス | 辞書 | 子 | エレメントのリスト |
find, findall メソッドを使用する場合、名前空間の指定に辞書を使います。
肝心のデフォルト名前空間の指定方法です。
XML内でのデフォルト名前空間:xmlns="http://www.w3.org/2005/Atom"
findメソッドで使う辞書の指定:ns={"":"http://www.w3.org/2005/Atom"}
◎xpathで検索
コレクションURIが返す XML のlinkタグは属性が異なるものが複数存在するので、属性も含めて解析します。
そのためには XPath を指定できる find() メソッドを使用します。
ElementTree の XPath サポートは、要素の位置決めのために必要な構文の一部のみサポートしています。
次の説明は、その中のさらに一部です。
XPathは属性もノードとみなされます。
ノードの区切りは /
で指定します。
【主な構文】
tag
:タグ名*
:すべての子供.
:現在のノード//
:すべての子孫..
:親[@attrib]
:属性を持つ要素[@attrib="value"]
:指定した値を持つ属性を持つ要素[@attrib!="value"]
:指定した値を持たない属性を持つ要素
◎例
ns = {'': 'http://www.w3.org/2005/Atom'} # デフォルト名前空間の指定 next_el = root.find(".link/[@rel='next']", ns)
◎コード
▽XMLの解析
def get_article_info(self, xml:str) -> Tuple[str, dict]: """ コレクション応答XMLから記事の情報を取得 Args: str: XML文書 Returns: str: 次の記事のurl dict: 記事の情報の辞書(キーはURL、値は辞書) """ root = ET.fromstring(xml) ns = {'': 'http://www.w3.org/2005/Atom'} # デフォルト名前空間の指定 # 次ページ用タグの取得 # linkタグの属性relがnextのものを検索。無い場合を考慮 next_el = root.find(".link/[@rel='next']", ns) if ET.iselement(next_el): # エレメント自体を判定するとうまくいかない next_ = next_el.get("href") # urlの取得 else: next_ = None article_info = {} entry_iter = root.iter("{http://www.w3.org/2005/Atom}entry") # iterは名前空間辞書は使えない for entry_ in entry_iter: # entryタグの子から記事の属性を取得 link_ = entry_.find(".link/[@rel='alternate']", ns).get("href") # 記事のURL title_ = entry_.find("title", ns).text # 記事のタイトル updated_ = entry_.find("updated", ns).text # 記事の更新日 updated_ = updated_.replace("+09:00", "") # csvをエクセルで開いた時に見やすくするための置換 updated_ = updated_.replace("T", " ") # yy:mm:ddThh:mm:ss+09:00⇒yy:mm:dd hh:mm:ss published_ = entry_.find("published", ns).text # 記事の投稿日 published_ = published_.replace("+09:00", "") published_ = published_.replace("T", " ") category_list = entry_.findall("category", ns) # 記事のカテゴリ category_ = [element_.get("term") for element_ in category_list] # カテゴリは複数あるのでまずリストに category_ = ",".join(category_) # Notionで扱いやすいようにカンマ区切りの文字列に # 取得した情報を辞書に、後でcsv出力しやすいように、キーはURL article_info[link_] = {"title":title_, "published":published_ , "updated":updated_, "category":category_} return next_, article_info
◆はてなスター
はてなスター取得 API は HTTP の GET を特定の URL に対して行うことで、ある URL に対して付与されたスターを取得できる REST API です。 Hatena Developer Center
◇スター取得API
次のURIに対してGETリクエストします。
https://s.hatena.com/entry.json?uri=「記事URL」
◇応答
GETリクエストの結果として以下をJSON形式で取得できます。
※[]
という表現が正しいかどうかわかりませんが、配列を表現してみました
- entries:以下を含む0個以上の配列
◇取得に失敗した場合
取得に成功した場合、応答は 200 です。
失敗した場合、以下のいずれかが返ります。
- 400 (Bad Request)
- 401 (Authorization Required)
- 200 (OK)※ただし、エラーメッセージを含んだ JSON データが返る
◇Pythonでの実装
Pythonのrequestsパーケージのget()メソッドで要求を出した場合、応答が戻り値で返ってきます。
戻り値はResponseオブジェクトです。
戻り値のResponseオブジェクトのjson()メソッドを使用して JSON 形式の結果をエンコードしたオブジェクトを取得できます。
stars 要素は基本的にはスターの数だけ存在するようです。
ただし、仕様的には、count 要素がある場合にはその数を使います。
黄色以外のスターは別要素になっているので共通のメソッド(get_star_num)になるように対応しています。
◇コード
def get_star_num(self, stars) -> int: """ はてなスターのタグから数を集計。countタグがある時はその数、無い時は1 タグは、name, quote, countのリスト。countはない場合がある Args: list: スター情報のリスト Returns: int: スターの数 """ item_num = len(stars) for star_ in stars: if "count" in star_: item_num = item_num + star_.get("count") - 1 return item_num def get_hatena_stars(self, url:str) -> dict: """ はてなスター情報をリクエストして取得。スターの色ごとに数を値にした辞書を作成 情報は、uri, starts, colored_starsのリスト。colored_starsはない場合がある colored_starsはcolor, starsのリスト。 Args: str: スターを集計する記事のURL Returns: dict: 色ごとのスターの数の辞書 """ endpoint = f"https://s.hatena.com/entry.json?uri={url}" r = requests.get(endpoint) stars_num = {"yellow":0, "green":0, "red":0, "blue":0, "purple":0} if r.status_code == 200: for entry_ in r.json().get("entries"): item_num = self.get_star_num(entry_.get("stars")) # 黄色スターの数を求める stars_num["yellow"] += item_num if entry_.get("colored_stars"): # 黄色以外のスターの数を求める for colored_star_ in entry_.get("colored_stars"): n = self.get_star_num(colored_star_.get("stars")) stars_num[colored_star_.get("color")] += n return stars_num
◆はてなブックマーク
GET リクエストでのシンプルな件数取得 API です。 Hatena Developer Center
◇はてなブックマーク件数取得API
GET リクエストでのシンプルなAPI
https://bookmark.hatenaapis.com/count/entry?url=「記事URL」
応答は ブックマーク数を値とした JSON 形式GET リクエストでのシンプルな API(複数 URL 版)最大50個
https://bookmark.hatenaapis.com/count/entries?url=「記事URL」&url=「記事URL」
応答は URL をキー、ブックマーク数を値とした JSON 形式
◇Pythonでの実装
Pythonのrequestsパーケージのget()メソッドで要求を出した場合、応答が戻り値で返ってきます。
戻り値はResponseオブジェクトです。
戻り値のResponseオブジェクトのjson()メソッドを使用して JSON 形式の結果をエンコードしたオブジェクトを取得できます。
はてなブックマークの場合、ブックマーク数が数値で返ります。
◇コード
def get_hatena_bookmark(self, url:str) -> int: """ はてなブックマーク情報をリクエストして取得。ブックマークの数を返す Args: str: ブックマークを集計する記事のURL Returns: int: ブックマークの数 """ endpoint = f"https://bookmark.hatenaapis.com/count/entry?url={url}" r = requests.get(endpoint) j = None if r.status_code == 200: j = r.json() # 個数が返る return j
◆アイキャッチ画像のURLの取得
別記事で紹介しています。
◆CSV出力
今回、CSVファイルに出力するデータは、リストやタプルではなく辞書にしました。
辞書にすることでデータ作成時の順番を気にする必要が無くなります。
また、辞書の場合、csv.DictWriter()
メソッドを使用することで簡単に CSV に出力することができます。
DictWriter() メソッドの第2引数にカラム定義を辞書のキーで指定すれば、その順番にカラムが出力されます。
また、今回、エクセルで CSV ファイルを読み込んだ時に日本語が文字化けしないようにエンコードをBOM付UTF8にしてあります。
◇コード
def output_results2csv(self, blog_info:list): """ XML出力 ファイルはカレントディレクトリに「hatenablog_sb_yymmddHHMM.xml」 エンコードはBOM付UTF-8。BOM付だとエクセルで文字化けしない。 Args: str: xml string """ logfile_name = f"hatenablog_sb_{datetime.now().strftime('%y%m%d%H%M')}.csv" try: with open(logfile_name, mode="w", encoding="utf_8_sig", newline="") as file_: # 辞書から出力するので辞書のキーをヘッダーとして定義する csv_fields = ["url", "title", "published", "updated", "bookmark", "yellow", "green", "red", "blue", "purple", "category", "eye_catch"] csv_writer = csv.DictWriter(file_, fieldnames=csv_fields) csv_writer.writeheader() csv_writer.writerows(blog_info) except Exception as e: print(f"書き込みエラー:{e}")
更新:2022-08-02
◆全体処理
【処理】
ブログのURLを初期化する
指定したページ数以内なら以下を繰り返す
リストをCSVに出力する
◇コード
▽全体の処理
hatena_atom = HatenaBlogAtom() pages = settings_hatena_url.pages # 取得するトップページのページ数 page = 1 # 処理しているページ next_blog_url = None articles_info = [] print(f"Start from URl:{settings_hatena_url.blog_id}") # pagesが0ならずっと、指定されていればそのページまで処理を繰り返す while page <= pages or pages == 0: # DEBUG debug = False if debug: # 取得したxmlファイルで動作確認 path = r'hatenaxml_220524.xml' with open(path, mode="r", encoding="utf-8") as f: result_xml = f.read() else: result_xml = hatena_atom.get_hatena(next_blog_url) # はてなブログへリクエストと結果取得 # hatena_atom.output_xml(result_xml) # デバッグ、解析用にxml出力 if not result_xml: print("\nエラー終了:URLが存在しないか、ユーザ ID か API key が誤っています") input("\n確認したらEnterキーを押してください") sys.exit(1) # はてなブログのコレクションを解析して記事情報を取得 next_blog_url, article_info1 = hatena_atom.get_article_info(result_xml) print(f"Next URL:{next_blog_url}") # はてなスター、ブックマークの数を取得 page_result_ = hatena_atom.get_star_and_bookmark_from_urls(article_info1) articles_info += page_result_ # リストにリストを追加 # 次ページのURIが無くなったら終了 if next_blog_url: page += 1 else: break hatena_atom.output_results2csv(articles_info) print("Finished") input("\n確認したらEnterキーを押してください")
▽記事ごとの処理
def get_star_and_bookmark_from_urls(self, blog_dict:dict) -> list: """ はてなスターの数とはてなブックマークの数を取得して付加する Args: dict: 記事情報の辞書(キーはURL)取得した記事の数だけある Returns: list: 記事属性(辞書)のリスト """ article_info = [] for url, value in blog_dict.items(): stars_dict = self.get_hatena_stars(url) # はてなスター数の取得(色をキーにした辞書) bookmark_num = self.get_hatena_bookmark(url) # はてなブックマーク数の取得 stars_dict["url"] = url # スターの辞書にURLを追加 stars_dict["bookmark"] = bookmark_num # スターの辞書にブックマーク数を追加 eye = self.get_hatena_eye_catch(url) # アイキャッチ画像のURLを取得 stars_dict["eye_catch"] = eye # スターの辞書にアイキャッチ画像のURLを追加 stars_dict.update(value) # スターの辞書に記事情報(辞書)を追加 article_info.append(stars_dict) # スターの辞書を記事属性の辞書としてリストに追加 return article_info
更新:2022-08-02
◆必要なパッケージ
- requests
【インストール】pip install requests
【インポート】 import requests
◆全体のソース
全体のソースはこちらから取得できます。
取得先:GitHub juu7g/Python-get-hatena-info
◆さいごに
はてなスターの数の取得は、返ってくるデータに癖があるので少し面倒でした。
この記事の公開準備をしている時にスターの数ではなく、スターを付けてくれた人の数を出してあげた方が需要があるかなと思い始めました。
需要という点では、先日、本アプリを公開しましたが、アプリはダウンロードされていません。
需要がないのか、記事に魅力がないのか、両方なのか、何か測るすべはないものか思案中です。
はてなブログの管理者の方が使えるようなツールをいくつか提供していますが、一つもダウンロードされていません。
なぜ使われないのか、難しいのか、需要がないのか、何かフィードバックがあると助けになるのですが・・・
期待してはいけませんね。
自分では使っているので、自分でフィードバックしていきます。
でも、誰かが使ってくれているのが分かると作り甲斐があるものですから・・・
◇ご注意
本記事は次のバージョンの下で動作した内容を基に記述しています。
- Python 3.8.5
- Requests 2.25.1
ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
◆参考
- はてなブログ:はてなブログAtomPub - Hatena Developer Center
- はてなスター:はてなスター取得 API - Hatena Developer Center
- はてなブックマーク:はてなブックマーク件数取得API - Hatena Developer Center