やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

はてなブログAPIをPython3で実行できた

前回のつづき。

問題のエラー

$ python3 main.py 
Traceback (most recent call last):
  File "main.py", line 95, in <module>
    'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True)
  File "main.py", line 48, in post_entries
    self._check_response(res)
  File "main.py", line 82, in _check_response
    response.raise_for_status()
  File "/usr/lib/python3/dist-packages/requests/models.py", line 773, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found

404エラー?限定公開だけど存在するブログのはず。

URLの指定ミス

と思ってコードをよく見てみたら単なるURL指定ミス。.hatenablog.comの部分がテンプレートになっていた。

    ENDPOINT = ('https://blog.hatena.ne.jp/'
                '{user}/{blog}.hatenablog.com/atom/entry')

あれ、ドメイン名の後半っていくつかパターンなかったっけ?はてなブログ作成するときに選択したような気がする。

API仕様

はてなブログAtomPub - Hatena Developer Center

仕様によるとhttps://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/{entry_id}

ブログID書式:ブログのドメイン (例: hoge.hatenablog.com)

ドメイン選択

また、ブログを作成 - はてなブログで作成するときに、以下のようなドメインを選択できるっぽい。

  • .hatenablog.com
  • .hatenablog.jp
  • .hateblo.jp
  • .hatenadialy.com
  • .hatenadialy.jp

f:id:ytyaru:20170226150333g:plain

修正

というわけで、以下のように修正する。

    ENDPOINT = ('https://blog.hatena.ne.jp/'
                '{user}/{blog}/atom/entry')
if __name__ == '__main__':
    client = HatenaClient(**CREDENTIALS)
    client.post_entries(
        'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True)

ドメイン名をすべて含めるようにする。はてなAPIではブログIDというらしい。

実行

$ python3 main.py 
update hatena blog
status code: 201

OKはてなブログから記事が投稿されていることを確認した。

最終コード

http://kiito.hatenablog.com/entry/2016/11/23/225729

ほぼ丸パクリさせていただいた。感謝。動作させるのに必要だった修正箇所はコメントアウトしてある。

#!python3
#encoding:utf-8
import xmltodict
from collections import OrderedDict
from requests_oauthlib import OAuth1Session
from bs4 import BeautifulSoup

CREDENTIALS = {
    'client_key': 'YOUR CONSUMER KEY',
    'client_secret': 'YOUR CONSUMER SECRET',
    'resource_owner_key': 'YOUR ACCESS TOKEN',
    'resource_owner_secret': 'YOUR ACCESS TOKEN SECRET'
}

class HatenaClient(object):
    ENDPOINT = ('https://blog.hatena.ne.jp/'
                '{user}/{blog}/atom/entry')
#                '{user}/{blog}.hatenablog.com/atom/entry')

    def __init__(self, **args):
        self.set_client(**args)

    def set_client(self, **args):
        self.client = OAuth1Session(**args)

    def post_entries(self, user, blog, title, body, categries=[], draft=True):
        url = self.ENDPOINT.format(user=user, blog=blog)
        xml = self._create_body_xml(title, body, categries, draft)
#        res = self.client.post(url, data=xml)
        res = self.client.post(url, data=xml, headers={'Content-Type': 'application/xml; charset="UTF-8"'})
        self._check_response(res)
        return self._parse_to_url(res)

    def _create_body_xml(self, title, body, categories, draft):
        body = OrderedDict([
            ('entry', OrderedDict([
                ('@xmlns', 'http://www.w3.org/2005/Atom'),
                ('@xmlns:app', 'http://www.w3.org/2007/app'),
                ('title', title),
                ('author', OrderedDict([('name', 'name')])),
                ('content', OrderedDict([
                    ('@type', 'text/plain'),
                    ('#text', body)
                ])),
                ('category', self._create_categories(categories)),
                ('app:control', OrderedDict([
                    ('app:draft', self._is_draft(draft))
                ]))
            ]))])
        return xmltodict.unparse(body).encode('utf-8')

    def _is_draft(self, draft):
        if draft:
            return 'yes'
        return 'no'

    def _create_categories(self, categories):
        if not categories:
            return None
        return [OrderedDict([('@term', c)]) for c in categories]

    def _check_response(self, response):
        if not response.ok:
            response.raise_for_status()
        print('update hatena blog')
        print('status code: {}'.format(response.status_code))

    def _parse_to_url(self, response):
        soup = BeautifulSoup(response.text, 'lxml')
        return soup.find('link', rel='alternate').get('href')


if __name__ == '__main__':
    client = HatenaClient(**CREDENTIALS)
    client.post_entries(
        'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True)
#        'takeshi0406', 'fudosaninfo', 'title', '本文', draft=True)

課題

  • 予約投稿ができるか確認したい

所感

PythonでもはてなAPIの動作確認ができた。あとはAPI仕様を調べながらやるだけ。