Pythonのrequestsモジュールでの文字コード対策
【Webスクレイピング Advent Calendar 2017 4日目の記事です。】
Pythonのrequests
モジュールは、 「Requestsは、人が使いやすいように設計されていて、Pythonで書かれている Apache2 Licensed ベースのHTTPライブラリです。」 と公式サイト1文目に記述されているほど、扱いやすいHTTPライブラリです。
そんなrequests
モジュールですが、日本語HTMLを対象に取得する際に文字化けを起こすことがしばしばあります。
その対策や原因について備忘録としてまとめます。
対策まとめ
requests
単体でhtmlを取り出したい場合は、response.encoding = response.apparent_encoding
とするBeautifulsoup
と組み合わせる場合は、soup = BeautifulSoup(response.content, "lxml")
と、バイト列をBeautifulSoup
に渡す
モジュールのバージョンなど
- Python (3.6.1)
- requests (2.14.2)
- chardet (3.0.3)
- cchardet (2.1.1)
- beautifulsoup4 (4.6.0)
レスポンスヘッダに文字エンコード情報がないため起こる文字化け
requests
モジュール単体で、ページからtext
を取得するときの話です。
本当はSHIFT JISのページですが、requestsモジュールではISO-8859-1と判定されてしまう場合の例です。(架空のURLです)
>>> response = requests.get("http://www.example.com/") >>> print(response.encoding) 'ISO-8859-1'
HTML内では、meta情報でcontent="text/html;charset=shift_jis"
と記述されています。
なぜ文字化けするのでしょうか。
文字化けの原因
requestsモジュールのソースコードを見てみると、get_encoding_from_headers
メソッドで文字コードを識別しています。
このメソッドは、HTTPのレスポンスヘッダ内のcontent-type
に含まれるcharset
を読みに行きます。
したがってレスポンスヘッダに文字コード情報が記述されていない場合は、デフォルト値のISO-8859-1
が設定されてしまいます。
対策
requests
モジュールでは、取得したHTMLに含まれるテキスト情報から、文字コードを推定してくれる機能があります。
使い方は以下のように、apparent_encoding
をencoding
に指定します。
>>> response.encoding = response.apparent_encoding
これによりresponse.textで文字化けしていない文字が抽出できるようになります。
requests
モジュールのソースコードを見てみると、apparent_encoding
はプロパティとなっており、呼び出す度に文字コード推定モジュールchardet
を用いて、HTMLのバイト列から文字コードを推定しています。
大量のページをダウンロードするときは、cChardet
日本語のサイトでは、apparent_encoding
を設定しておくことが無難に思えます。
この文字コード自動推定はちょっとしたボトルネックになりがちで、大量のページをダウンロードする際は、時間を要します。
この文字コード推定をcChardet
に置き換えることで、高速化できます。
公式のベンチマークによると、chardet
が0.35(call/s)に対して、cchardet
は1467.77(call/s)となります。
ベンチマークは、ある1ファイル(1512行、約16万文字)に対して行われているようです。
インストールはpip install cchardet
でできます。
使い方は、以下のようにapparent_encoding
のかわりに、cchardetによる文字コード推定結果を用いるだけです。
>>> import requests >>> import cchardet >>> response = requests.get("http://www.example.com/") >>> response.encoding = cchardet.detect(response.content)["encoding"] >>> print(response.encoding) 'SHIFT_JIS'
BeautifulSoupと組み合わせて使う
requests
で取得したHTMLを解析するためにBeautifulSoup
を用いる場合が多いと思います。
BeautifulSoup
は、バイト文字列を読み込んで文字コードを推定する機能があるため、cchardet
でエンコード情報を再設定する必要はありません。
>>> import requests >>> from bs4 import BeautifulSoup >>> response = requests.get("http://www.example.com/") >>> soup = BeautifulSoup(response.content, "lxml") >>> print(soup.original_encoding) shift_jis
BeautifulSoup
の内部ではUnicodeDammit
クラスにより、文字コードを判定しています。
ソースコードを読むと、文字コード推定の砦がいくつかあるようで、HTMLバイト列の先頭バイト列を見て文字コードを決定したり、chardet
を使っているようです。
どうしてもcchardet
を用いたい場合は、from_encoding
で文字コード情報を渡すことができます。
>>> soup = BeautifulSoup(response.content, "lxml", from_encoding=cchardet.detect(response.content)["encoding"])
どちらが良いのかは、まだよく分かっていません。
まとめ
何度も忘れては調べるrequests
の文字コード周りを書きながら、自分の中で整理しました。
結論としては、人間が設定しないといけないレスポンスヘッダやHTML内のメタタグは最初から信用せず、バイト列を元に推定すると良いというお話でした。
参考にしたサイト
- BeautifulSoup Doc.日本語訳より、 エンコード、Unicode, Dammit
- RequestsとBeautiful Soupでのスクレイピング時に文字化けを減らす
- pythonとBeautifulsoupとrequests