RequestsとBeautiful Soupでのスクレイピング時に文字化けを減らす
多様なWebサイトからスクレイピングする際、Webサイトによっては文字化けが発生することがあります。 RequestsとBeautiful Soupを組み合わせる場合に、なるべく文字化けを減らす方法を解説します。
Beautiful Soupはパーサーを選択できますが、ここではhtml.parserに絞って解説します*1。
結論
以下の2点を守ると概ね幸せです。 Content-Typeヘッダーのエンコーディングを参照するコードは下の方に掲載しています。
1. Chardetをインストールしておく。
$ pip install chardet
2. RequestsのResponseオブジェクトをr
としたとき、BeautifulSoupのコンストラクターには(r.text
ではなく)r.content
を渡す。
import requests from bs4 import BeautifulSoup r = requests.get(url) soup = BeautifulSoup(r.content, 'html.parser')
環境
- Python 3.6.0
- Requests 2.12.4
- Beautiful Soup 4.5.3 (パーサーはhtml.parserを使用)
- Chardet 2.3.0
Webページのエンコーディング
Webページのエンコーディングの指定・推定方法は以下の3つがあります。どれも間違っていることがあるので、これらを組み合わせて正しそうなエンコーディングを選択します。
- HTTPレスポンスのContent-Typeヘッダーのcharsetで指定されたエンコーディング
- この記事ではContent-Typeのエンコーディングと呼ぶ
- 正しくなかったり、charsetが指定されてなかったりすることがある(特に静的ページの場合)
- HTMLの<meta>タグまたはXMLのXML宣言で指定されたエンコーディング
- この記事では<meta>タグのエンコーディングと呼ぶ
- 概ね正しいことが多い
- HTTPレスポンスのバイト列から推定されたエンコーディング
- この記事では推定されたエンコーディングと呼ぶ
- ある程度の長さがあれば概ね正しく推定できる
- 上記2つに比べて処理に時間がかかる
Requestsとエンコーディング
RequestsではResponseオブジェクトをr
とすると次のようになります。
r.encoding
: Content-Typeのエンコーディングr.apparent_encoding
: 推定されたエンコーディング *2r.content
: bytes型のレスポンスボディr.text
:r.encoding
でデコードされたstr型のレスポンスボディ
以下の点は注意が必要です。
- Requestsは<meta>タグのエンコーディングは見ない(HTTPのライブラリなので)
- Content-Typeにtextが含まれていてcharsetがない場合、
r.encoding
は'ISO-8859-1'
となる *3*4- この場合、日本語のサイトでは
r.text
が文字化けする
- この場合、日本語のサイトでは
Beautiful Soupとエンコーディング
コンストラクターBeautifulSoup()
の第1引数には、HTMLの文字列をstr型またはbytes型で指定できます。
str型のHTML文字列を渡した場合は、Beautiful Soup側ではエンコーディングに関しては特に何もしません。
bytes型のHTML文字列を渡した場合は、Beautiful Soup側でstr型にデコードされます。以下のエンコーディングによるデコードを順に試して、正しくデコードできたものが使われます*5。
どうしたら文字化けしないのか
ここまで見てきたように、Requestsのr.encoding
やr.text
をそのまま使うと、文字化けしやすくなります。
基本的な戦略としては、以下の2点を守るのがオススメです。
- Chardetをインストールしておく
BeautifulSoup()
にr.content
を渡してBeautiful Soup側でデコードする
大体OKなのでオススメなコード
記事冒頭にも書きましたが、Chardetをインストールした上で、次のようにすると大体の場合文字化けを回避できます。 シンプルなのでオススメです。
import requests from bs4 import BeautifulSoup r = requests.get(url) soup = BeautifulSoup(r.content, 'html.parser')
このコードでは次の順でエンコーディングを見ます。
Content-Typeヘッダーの指定を尊重したい場合のコード
Content-Typeヘッダーの指定を尊重したい場合は、次のようにできます。
r.encoding
が'ISO-8859-1'
の場合は無視することで、Content-Typeヘッダーにcharsetが指定されていない時に文字化けするのを回避できます。ただし、Content-Typeヘッダーが間違っていた場合には文字化けすることがあります*6。
import requests from bs4 import BeautifulSoup r = requests.get(url) content_type_encoding = r.encoding if r.encoding != 'ISO-8859-1' else None soup = BeautifulSoup(r.content, 'html.parser', from_encoding=content_type_encoding)
このコードでは次の順でエンコーディングを見ます。
まとめ
なるべく文字化けに遭遇することなくスクレイピングしたいですね。
Pythonクローリング&スクレイピングではライブラリを個別に紹介していて、組み合わせたときの話はあまり詳しく書いていませんでした。普段はBeautiful Soupよりlxml推しですが、Beautiful Soupを使う機会があったのでまとめておきました。
*1:軽く試した範囲では、lxmlはhtml.parserと同様の結果になり、html5libは推定されたエンコーディングの判別に失敗することがありました。html5libは先頭100バイトしかChardetに渡さないようです。html5libを使う場合は、「Content-Typeヘッダーの指定を尊重したい場合のコード」を使ったほうが良いかもしれません。
*4:ISO-8859-1はRFC 2616で定められたデフォルト値です。RFC 7231ではこのデフォルト値はなくなったので、3.0に向けての議論はありますが、Kenneth Reitz氏はあまり乗り気ではなさそうです。
*5:詳しくはbs4/builder/_htmlparser.pyやbs4/dammit.pyを参照
*6:from_encodingに指定したエンコーディングで正しくデコードできない場合は無視されるので、文字化けしないこともあります。