前回のつづき。
問題のエラー
$ 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
修正
というわけで、以下のように修正する。
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
最終コード
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)
課題
- 予約投稿ができるか確認したい