出力を入力へ

プログラミングに関する基本的な事を中心にまとめます

Requestsで取得したwebサイトの文字化けに対処する

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サイトの文字コードを正しく識別できていないことが原因. これについては,以下のサイトが詳しい.

orangain.hatenablog.com

実際,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によるとどちらでもよいみたい.

tools.ietf.org

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内で文字コードを指定されているのであればそれに従う方法もある.