Requestsで取得したWebサイトをlxmlでスクレイピングしようとすると, いくつかのサイトで文字化けすることに気が付いた.
これに対処するのに苦労したし,何ならまだ完全には解決できていない. 間違いとかあれば教えて欲しい.
文字化けの確認
例えば O'reillyのサイトだと文字化けが発生する. webサイトのタイトルを出力するPythonスクリプトは以下の通り.
import requests import lxml.html res = requests.get('https://www.oreilly.co.jp/books/9784873118864/') root = lxml.html.fromstring(res.content) print(root.xpath('//title')[0].text)
このスクリプトの実行結果は以下の通り
O'Reilly Japan - ã¬ã¬ã·ã¼ã³ã¼ãããã®è ´
上記サイトはタイトルに「O'Reilly Japan - レガシーコードからの脱却」が指定されており, この通り出力して欲しい.
このブログでもダメ.
'å\x87ºå\x8a\x9bã\x82\x92å\x85¥å\x8a\x9bã\x81¸'
一方で,Yahooニュースなら正常に動作する.
'Yahoo!ニュース'
原因
もちろんエンコーディングの指定誤りが原因で, Requests で取得したwebサイトの文字コードを正しく識別できていないことが原因. これについては,以下のサイトが詳しい.
実際,HTTPレスポンスを確認すると
$ wget --server-response https://www.oreilly.co.jp https://www.oreilly.co.jp/books/9784873118864/ ... HTTP request sent, awaiting response... HTTP/1.1 200 OK Date: Sat, 12 Oct 2019 16:42:53 GMT Content-Type: text/html Content-Length: 23094 Connection: keep-alive Server: Apache Last-Modified: Thu, 10 Oct 2019 05:06:47 GMT ETag: "5a36-594875e4b2e01" Accept-Ranges: bytes Vary: Accept-Encoding ... $ wget --server-response https://thaim.hatenablog.jp/ ... HTTP request sent, awaiting response... HTTP/1.1 200 OK Server: nginx Date: Sat, 12 Oct 2019 16:25:25 GMT Content-Type: text/html; charset=utf-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Vary: User-Agent, X-Forwarded-Host, X-Device-Type Access-Control-Allow-Origin: * Content-Security-Policy-Report-Only: block-all-mixed-content; report-uri https://blog.hatena.ne.jp/api/csp_report P3P: CP="OTI CUR OUR BUS STA" X-Cache-Only-Varnish: 1 X-Content-Type-Options: nosniff X-Dispatch: Hatena::Epic::Web::Blogs::Index#index X-Frame-Options: DENY X-Page-Cache: hit X-Revision: a7694746800267be0e2d318311d7b13e X-XSS-Protection: 1 X-Runtime: 0.042860 X-Varnish: 34747135 Age: 0 Via: 1.1 varnish-v4 X-Cache: MISS Cache-Control: private Accept-Ranges: bytes ... $ wget --server-response https://news.yahoo.co.jp ... HTTP request sent, awaiting response... HTTP/1.1 200 OK Cache-Control: private, no-cache, no-store, must-revalidate Content-Type: text/html;charset=UTF-8 Date: Sat, 12 Oct 2019 16:26:09 GMT Set-Cookie: B=1qlmjdleq3vl1&b=3&s=l0; expires=Tue, 12-Oct-2021 16:26:09 GMT; path=/; domain=.yahoo.co.jp Vary: Accept-Encoding X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: DENY X-Vcap-Request-Id: 9826dce8-3f25-4d7b-7749-c0e9fdcd8fba X-Xss-Protection: 1; mode=block Age: 0 Server: ATS Transfer-Encoding: chunked Connection: keep-alive Via: http/1.1 edge2502.img.umd.yahoo.co.jp (ApacheTrafficServer [c sSf ]) Set-Cookie: XB=1qlmjdleq3vl1&b=3&s=l0; expires=Sat, 19-Oct-2019 16:26:09 GMT; path=/; domain=.yahoo.co.jp; secure; samesite=none ...
そう,実ははてなブログではContent-Typeが適切に設定されているのに上手くいかない. 文字コードを小文字で指定しているのが原因かとも思ったけれど RFCによるとどちらでもよいみたい.
requestsは chardetで文字コードの 推定も行っているので確認してみたけど, ブログもYahooニュースもどちらもUTF-8を認識している.
>>> import requests, lxml.html >>> res = requests.get('https://thaim.hatenablog.jp/') >>> res.encoding 'utf-8' >>> res.apparent_encoding 'utf-8' >>> res = requests.get('https://news.yahoo.co.jp/') >>> res.encoding 'UTF-8' >>> res.apparent_encoding 'utf-8'
これについてはお手上げで, なぜYahooニュースでは上手くいくのに はてなブログでは上手くいかないのかわからなかった.
対策
根本原因がどうであれ, 正しく文字コードを指定して処理すればいいだけなので, この問題を解決するだけならlxmlでスクレイピングする前に文字コードを指定してデコードしてあげればよい.
意図した通り動作するスクリプトは以下の通り.
import requests import lxml.html res = requests.get('https://www.oreilly.co.jp/books/9784873118864/') root = lxml.html.fromstring(res.content.decode('utf-8')) print(root.xpath('//title')[0].text)
ここでは,UTF-8で固定しているけれど,
res.apparent_encoding
を指定したり,HTML内で文字コードを指定されているのであればそれに従う方法もある.