前回のつづき。今回は表題のエラーが解決できなかったログ。
Error
問題のエラー。
python3 main.py Traceback (most recent call last): File "main.py", line 73, in <module> 'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True) File "main.py", line 27, in post_entries res = self.client.post(url, data=xml) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 497, in post return self.request('POST', url, data=data, **kwargs) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 421, in request prep = self.prepare_request(req) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 359, in prepare_request hooks=merge_hooks(request.hooks, self.hooks), File "/usr/lib/python3/dist-packages/requests/models.py", line 291, in prepare self.prepare_auth(auth, url) File "/usr/lib/python3/dist-packages/requests/models.py", line 470, in prepare_auth r = auth(self) File "/usr/local/lib/python3.4/dist-packages/requests_oauthlib/oauth1_auth.py", line 80, in __call__ unicode(r.url), unicode(r.method), r.body or '', r.headers) File "/usr/local/lib/python3.4/dist-packages/oauthlib/oauth1/rfc5849/__init__.py", line 276, in sign "Headers indicate a formencoded body but body was not decodable.") ValueError: Headers indicate a formencoded body but body was not decodable.
分析
HeaderのContent-Type
がapplication/x-www-form-urlencoded
になっているけど、送信するデータがURLエンコードされていないという意味だろうか?
以下のコードでurllib.parse.urlencode
してみたが違うエラーがでた。
#!python3 #encoding:utf-8 import xmltodict from collections import OrderedDict from requests_oauthlib import OAuth1Session from bs4 import BeautifulSoup import urllib CREDENTIALS = { 'client_key': 'aaaaaaaaaaaaaaaa', 'client_secret': 'bbbbbbbbbbbbbbbbb', 'resource_owner_key': 'ccccccccccccccc', 'resource_owner_secret': 'dddddddddddddddddddddd' } class HatenaClient(object): ENDPOINT = ('https://blog.hatena.ne.jp/' '{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) 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) ('#text', urllib.parse.urlencode(body).encode(encoding='utf-8')) ])), ('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)
$ python3 main.py Traceback (most recent call last): File "/usr/lib/python3.4/urllib/parse.py", line 760, in urlencode raise TypeError TypeError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "main.py", line 78, in <module> 'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True) File "main.py", line 29, in post_entries xml = self._create_body_xml(title, body, categries, draft) File "main.py", line 44, in _create_body_xml ('#text', urllib.parse.urlencode(body).encode(encoding='utf-8')) File "/usr/lib/python3.4/urllib/parse.py", line 768, in urlencode "or mapping object").with_traceback(tb) File "/usr/lib/python3.4/urllib/parse.py", line 760, in urlencode raise TypeError TypeError: not a valid non-string sequence or mapping object
文字列として認識されていないという意味か?
整理
- ヘッダの設定が見当たらない
- 本当に
Content-Type
:application/x-www-form-urlencoded
か?- 根拠は
ValueError: Headers indicate a formencoded body but body was not decodable.
- 根拠は
- 仮にそうだとして、どうすればいいか?
ヘッダの設定はコードにはない。デフォルト設定なのだろう。
そもそもContent-Type
がapplication/x-www-form-urlencoded
で送信することが間違っているのでは?
次に、ライブラリでヘッダ情報を設定する方法を調べる。
- OAuth1Session
はてなブログAPI仕様を調べる
はてなブログAtomPub - Hatena Developer Center
「ブログエントリの投稿」の項をみてもヘッダについての仕様はなかった。Content-Typeは何にすればいいんだ?
たぶんxmlなので以下でいいのだろう。
Content-Type: application/xml; charset="UTF-8"
xmlのContent-Typeはapplication/xmlか、それともtext/xml? - toricor’s memo
OAuth1Sessionのヘッダ設定方法を調べる
ソースコードをみてみる。
requests-oauthlib/oauth1_auth.py at master · requests/requests-oauthlib · GitHub
... CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded' ... content_type = r.headers.get('Content-Type', '') ... content_type = CONTENT_TYPE_FORM_URLENCODED ...
Content-Type
が未設定の場合はapplication/x-www-form-urlencoded
を設定するらしい。r
が何なのかわからないが、requestsライブラリのリクエストまたはレスポンスオブジェクトかもしれない。以下のようにrequests
ライブラリを使っているようだから。
from requests.auth import AuthBase
さて、どうやってヘッダを設定するのやら。
OAuth1Session header setで検索すると以下がヒット。
python - Setting HTTP User-Agent header with requests-oauthlib - Stack Overflow
oauthlib.oauth1.Client
を継承したクラスで設定できるらしい。たかがヘッダを編集するだけで大げさすぎないか?
class CustomClient(Client): def _render(self, request, formencode=False, realm=None): request.headers['User-Agent'] = "FooClient/1.0" return super()._render(request, formencode, realm)
oauth = OAuth1Session(CONSUMER_KEY, client_secret=CONSUMER_SECRET, signature_method=SIGNATURE_HMAC, signature_type=SIGNATURE_TYPE_AUTH_HEADER, client_class=CustomClient)
以下のようにコードを修正して実行。
#!python3 #encoding:utf-8 import xmltodict from collections import OrderedDict from requests_oauthlib import OAuth1Session from bs4 import BeautifulSoup import urllib #import oauthlib.oauth1.Client from oauthlib.oauth1 import Client #class CustomClient(oauthlib.oauth1.Client): class CustomClient(Client): def _render(self, request, formencode=False, realm=None): request.headers['Content-Type'] = 'application/xml; charset="UTF-8"' return super()._render(request, formencode, realm) CREDENTIALS = { 'client_key': 'aaaaaaaaaaaaaaaa', 'client_secret': 'bbbbbbbbbbbbbbbbb', 'resource_owner_key': 'ccccccccccccccc', 'resource_owner_secret': 'dddddddddddddddddddddd' 'client_class': CustomClient } } class HatenaClient(object): ENDPOINT = ('https://blog.hatena.ne.jp/' '{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) 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) # ('#text', urllib.parse.urlencode(body).encode(encoding='utf-8')) ])), ('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', 'new_title', 'contents.', draft=True) 'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True)
しかし同じエラー。
$ python3 main.py Traceback (most recent call last): File "main.py", line 87, in <module> 'ytyaru', 'blogmake.hatenablog.com', '新しい記事だよ', '本文です。', draft=True) File "main.py", line 39, in post_entries res = self.client.post(url, data=xml) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 497, in post return self.request('POST', url, data=data, **kwargs) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 421, in request prep = self.prepare_request(req) File "/usr/lib/python3/dist-packages/requests/sessions.py", line 359, in prepare_request hooks=merge_hooks(request.hooks, self.hooks), File "/usr/lib/python3/dist-packages/requests/models.py", line 291, in prepare self.prepare_auth(auth, url) File "/usr/lib/python3/dist-packages/requests/models.py", line 470, in prepare_auth r = auth(self) File "/usr/local/lib/python3.4/dist-packages/requests_oauthlib/oauth1_auth.py", line 80, in __call__ unicode(r.url), unicode(r.method), r.body or '', r.headers) File "/usr/local/lib/python3.4/dist-packages/oauthlib/oauth1/rfc5849/__init__.py", line 276, in sign "Headers indicate a formencoded body but body was not decodable.") ValueError: Headers indicate a formencoded body but body was not decodable.
post(headers={})
コードをよく見ると、post()
がrequestsライブラリのものと似ている。ダメ元でheaders
を渡してみたら表題のエラーをクリアできた。
# res = self.client.post(url, data=xml) res = self.client.post(url, data=xml, headers={'Content-Type': 'application/xml; charset="UTF-8"'})
しかし、今度は以下のエラーが出た。
$ 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エラー?存在しないわけ無いのだが。
所感
作者様のコードがヘッダ指定せずに動作するというのは、たぶんAPI仕様かライブラリの仕様などが変わったせいなのだろう。