はしくれエンジニアもどきのメモ

情報系技術・哲学・デザインなどの勉強メモ・備忘録です。

はてなブログAPIで記事一覧を取得

はてなブログAPIで記事一覧を取得

はてなブログAPIはてなブログAtomPub)で記事一覧取得のメモ. 「サービス文書URIを使って, コレクション操作の一覧の取得」と 「コレクションURIを使って,ブログエントリ一覧を取得」のメモ.

今回は,PythonでrequestsモジュールとBeautifulSoup4を使う.

ドキュメントはここです.はてなブログAtomPub - Hatena Developer Center

環境

  • Windows10
    • python 3.5.4
      • requests (2.18.4)
      • beautifulsoup4 (4.6.0)

Atom Publishing Protocol とは

Atom Publishing Protocol(以下 AtomPub) はウェブリソースを公開、編集するためのアプリケーション・プロトコル仕様です。はてなブログのAtomPubと通じて、開発者ははてなブログのエントリを参照、投稿、編集、削除するようなオリジナルのアプリケーションを作成できます。

はてなブログAtomPub APIを使うことで,ブログの記事の取得,投稿,編集,削除ができる.

## はてなブログAtomPub APIがサポートしている操作

主に4つある.

  • サービスの操作 (サービス文書URIを使う)
    • コレクション操作の一覧の取得
  • ブログの操作 (コレクションURIを使う)
    • ブログエントリ一覧の取得
    • ブログエントリの新規投稿
  • ブログエントリの操作 (メンバURIを使う)
    • ブログエントリの取得
    • ブログエントリの更新
    • ブログエントリの削除
  • カテゴリの操作 (カテゴリ文書URIを使う)
    • カテゴリ一覧の取得

今回は,以下の2つを行う.

  • サービス文書URIを使って, コレクション操作の一覧の取得
  • コレクションURIを使って,ブログエントリ一覧の取得

はてなAtomPubAPIの認証

はてなブログAtomPub を利用するために、クライアントは

のいずれかを行う必要があります。

書いてあるように,はてなAtomPubAPIでは,Basic認証とWSSE認証とOAuth認証が使える.

Basic認証のためのIDとパスワード

Basic認証についてはユーザ名としてはてなIDを、パスワードとしてAPIキーを利用することで認証できます。

APIキーは,はてなブログの「詳細設定 > AtomPub」から確認できるのでコピペしておく.

APIキーの確認

URIの表記

  • はてなID: 意味:あなたのはてなid
  • ブログID:ブログのドメイン (例: hoge.hatenablog.com)
  • entry_id: 意味:ブログエントリのID, カテゴリURIの記事一覧から取得できる.

サービス文書URIを使って, コレクション操作の一覧の取得

はてなブログ AtomPub で操作できるコレクションの一覧を含むサービス文書を取得できます。

コレクション操作(URI)の一覧を取得できる. 現在,コレクションURIは1つだけなので,その1つが返る.

レスポンスが正しく返れば,認証が正しくできているか確認もできる.

リクエス

GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom

requestsモジュールを利用したBasic認証

省略表記では,引数にauth=(<user_name>,<password>) を指定する.

requests.get(URI, auth=(<user_name>,<password>))

参考:Authentication — Requests 2.18.4 documentation

requestsモジュールでResponse確認

import requests

hatena_id = "<hatena id>"
blog_id = "<blog_id>"
password = "<api key>"

service_doc_uri = "https://blog.hatena.ne.jp/{hatena_id:}/{blog_id:}/atom".format(hatena_id=hatena_id, blog_id=blog_id)
res_service_doc = requests.get(url=service_doc_uri, auth=(hatena_id, password))
print(res_service_doc.text)
<?xml version="1.0" encoding="utf-8"?>
<service xmlns="http://www.w3.org/2007/app">
  <workspace>
    <atom:title xmlns:atom="http://www.w3.org/2005/Atom">はしくれエンジニアもどきのメモ</atom:title>
    <collection href="https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry">
      <atom:title xmlns:atom="http://www.w3.org/2005/Atom">&#x306F;&#x3057;&#x304F;&#x308C;&#x30A8;&#x30F3;&#x30B8;&#x30CB;&#x30A2;&#x3082;&#x3069;&#x304D;&#x306E;&#x30E1;&#x30E2; - 記事一覧</atom:title>
      <accept>application/atom+xml;type=entry</accept>
    </collection>
  </workspace>
</service>

サービス文書URIのレスポンスから,コレクションURIを取得する

サービス文書URIのレスポンスには,コレクションURIが含まれている.

<collection href="https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry">

これをBeautifulSoup4 を使って抽出する.

BeautifulSoup4でXMLを扱う場合, bs4.BeautifulSoup(XMLtext, features="xml") で扱える.

import requests
import bs4

hatena_id = "<hatena id>"
blog_id = "<blog_id>"
password = "<api key>"

def get_collection_uri(hatena_id, blog_id, password):
    service_doc_uri = "https://blog.hatena.ne.jp/{hatena_id:}/{blog_id:}/atom".format(hatena_id=hatena_id, blog_id=blog_id)
    res_service_doc = requests.get(url=service_doc_uri, auth=(hatena_id, password))
    if res_service_doc.ok:
        soup_servicedoc_xml = bs4.BeautifulSoup(res_service_doc.content, features="xml")
        collection_uri = soup_servicedoc_xml.collection.get("href")
        return collection_uri
    return False

get_collection_uri(hatena_id, blog_id, password)

results:

'https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry'

gist: はてなブログAPI サービス文書URIのレスポンスからコレクションURIを取得 · GitHub

コレクションURIを使って,ブログエントリ一覧を取得

はてなブログのエントリを操作するためのコレクションです。ブログエントリの一覧取得、新規ブログエントリの投稿を行うことができます。

ブログエントリの一覧取得

コレクション URI を GET することで、ブログエントリ一覧を取得できます。一度に7件のブログエントリを取得できます。また、取得したブログエントリ一覧が、コレクションの部分的リストである場合には、 page パラメータを付与する事で、7件目以降のブログエントリも取得出来ます。続きについては、AtomPub の仕様に基づき、部分的リストの続きは rel=next となる atom:link の href 属性がその URI となります。page パラメータを付与しない場合には、最新の7件を取得します。

どうやら現在では,最新7件でなく10件の記事を取得できる.

リクエス

GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry

最新10件以降の取得

GET https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=0123456789 // => GET <link rel="next">のhref属性

記事一覧の取得

import requests
import bs4

def get_collection_uri(hatena_id, blog_id, password):
    service_doc_uri = "https://blog.hatena.ne.jp/{hatena_id:}/{blog_id:}/atom".format(hatena_id=hatena_id, blog_id=blog_id)
    res_service_doc = requests.get(url=service_doc_uri, auth=(hatena_id, password))
    if res_service_doc.ok:
        soup_servicedoc_xml = bs4.BeautifulSoup(res_service_doc.content, features="xml")
        collection_uri = soup_servicedoc_xml.collection.get("href")
        return collection_uri

    return False

collection_uri = get_collection_uri(hatena_id, blog_id, password)
res_collection = requests.get(collection_uri, auth=(hatena_id, password))
print(res_collection.text)

results:

<?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="next" href="https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry?page=1504592364" />
  <title>はしくれエンジニアもどきのメモ</title>
  <subtitle>情報・Web系技術・Englishの勉強メモ・備忘録です。</subtitle>
  <link rel="alternate" href="http://cartman0.hatenablog.com/"/>
  <updated>2017-10-17T01:45:48+09:00</updated>
  <author>
    <name>cartman0</name>
  </author>
  <entry>
<id>tag:blog.hatena.ne.jp,2013:blog-cartman0-12921228815722929243-8599973812308668215</id>
<link rel="edit" href="https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry/8599973812308668215"/>
<link rel="alternate" type="text/html" href="http://cartman0.hatenablog.com/entry/2017/10/17/Slack_RSS_integrations_%E3%81%AEfetching%28polling%29%E3%81%AE%E4%BB%95%E6%A7%98_%28About_specifications_of_fetching%28polling%29_in_Slack_RSS_integrations%29"/>
<author><name>cartman0</name></author>
<title>Slack RSS integrations のfetching(polling)の仕様 (About specifications of fetching(polling) in Slack RSS integrations)</title>
<updated>2017-10-17T01:45:48+09:00</updated>
<published>2017-10-17T01:45:48+09:00</published>
<app:edited>2017-10-17T01:53:53+09:00</app:edited>
<summary type="text">Slack RSS integrations のfetching(polling)の仕様 (About specifications of fetching(polling) in Slack RSS integrations) 2017年9月時点の情報です. SlackのRSS…</summary>
<content type="text/x-markdown"># Slack RSS integrations のfetching(polling)の仕様 (About specifications of fetching(polling) in Slack RSS integrations)
...
</content>
...
<category term="Slack" />
<category term="RSS" />
<app:control>
  <app:draft>no</app:draft>
</app:control>
  </entry>
  • <link rel="next" href="https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry?page=1504592364" /> が次の10件のエントリがあるコレクションURIになる.
  • <entry>' > '<title>: 記事のタイトル
  • <entry> > <summary type="text"> :記事のサマリー
  • <content type="text/x-markdown">: 実際の投稿した記事の内容(はてな記法もそのまま)
    • type="text/x-markdown"markdown記法モードで投稿した場合
    • type=text/html:見たまま/htmlモードで投稿した場合
    • text/x-hatena-syntaxはてな記法で投稿した場合
  • <entry> > <id>tag:blog.hatena.ne.jp,2013:blog-cartman0-12921228815722929243-8599973812308668215</id>:idの最後のハイフンからの値8599973812308668215がentry idになる.
  • <entry> > <app:control><app:draft>no</app:draft></app:control> :は下書きかどうかを示している.yesの場合下書きの記事になる.

全記事のentry IDを取得

下書きの記事は今回無視して,全記事のentry id を取得する.

  • 無限ループを防ぐためにforループ使用.
  • サーバの負荷を減らすためにループの最後にsleepを入れる.
  • entry idは,<id>から正規表現で抽出
import requests
import bs4
import re
import time


def get_collection_uri(hatena_id, blog_id, password):
    service_doc_uri = "https://blog.hatena.ne.jp/{hatena_id:}/{blog_id:}/atom".format(hatena_id=hatena_id, blog_id=blog_id)
    res_service_doc = requests.get(url=service_doc_uri, auth=(hatena_id, password))
    if res_service_doc.ok:
        soup_servicedoc_xml = bs4.BeautifulSoup(res_service_doc.content, features="xml")
        collection_uri = soup_servicedoc_xml.collection.get("href")
        return collection_uri

    return False

collection_uri = get_collection_uri(hatena_id, blog_id, password)
entry_id_list = []

MAX_ITERATER_NUM = 50
for i in range(MAX_ITERATER_NUM):
    print(collection_uri)
    # Basic認証で記事一覧を取得
    res_collection = requests.get(collection_uri, auth=(hatena_id, password))
    if not res_collection.ok:
        print("faild")
        continue
    # Beatifulsoup4でDOM化
    soup_collectino_xml = bs4.BeautifulSoup(res_collection.content, features="xml")
    # entry elementのlistを取得
    entries = soup_collectino_xml.find_all("entry")
    # 下書きを無視
    pub_entry_list = list(filter(lambda e: e.find("app:draft").string != "yes", entries))
    # entry idを取得
    entry_id_list.extend([re.search(r"-(\d+)$", string=e.id.string).group(1) for e in pub_entry_list])

    # next
    link_next = soup_collectino_xml.find("link", rel="next")
    if not link_next:
        break
    collection_uri = link_next.get("href")
    if not collection_uri:
        break
    time.sleep(0.01)# 10ms

entry_id_list

results:

https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry
https://blog.hatena.ne.jp/cartman0/cartman0.hatenablog.com/atom/entry?page=1504592364
...
Out[]:
['8599973812308668215',
 '8599973812308570873',
...

関数版は,gistに置いておく.はてなブログAPI 全記事のentry idを取得 · GitHub