読者です 読者をやめる 読者になる 読者になる

orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

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')

環境

Webページのエンコーディング

Webページのエンコーディングの指定・推定方法は以下の3つがあります。どれも間違っていることがあるので、これらを組み合わせて正しそうなエンコーディングを選択します。

  1. HTTPレスポンスのContent-Typeヘッダーのcharsetで指定されたエンコーディング
    • この記事ではContent-Typeのエンコーディングと呼ぶ
    • 正しくなかったり、charsetが指定されてなかったりすることがある(特に静的ページの場合)
  2. HTMLの<meta>タグまたはXMLXML宣言で指定されたエンコーディング
  3. HTTPレスポンスのバイト列から推定されたエンコーディング
    • この記事では推定されたエンコーディングと呼ぶ
    • ある程度の長さがあれば概ね正しく推定できる
    • 上記2つに比べて処理に時間がかかる

Requestsとエンコーディング

RequestsではResponseオブジェクトをrとすると次のようになります。

以下の点は注意が必要です。

  • 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

  1. キーワード引数from_encodingで指定したエンコーディング
  2. <meta>タグのエンコーディング
  3. 推定されたエンコーディング(Chardetがインストールされている場合)

どうしたら文字化けしないのか

ここまで見てきたように、Requestsのr.encodingr.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')

このコードでは次の順でエンコーディングを見ます。

  1. <meta>タグのエンコーディング
  2. 推定されたエンコーディング

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)

このコードでは次の順でエンコーディングを見ます。

  1. Content-Typeのエンコーディング
  2. <meta>タグのエンコーディング
  3. 推定されたエンコーディング

まとめ

なるべく文字化けに遭遇することなくスクレイピングしたいですね。

Pythonクローリング&スクレイピングではライブラリを個別に紹介していて、組み合わせたときの話はあまり詳しく書いていませんでした。普段はBeautiful Soupよりlxml推しですが、Beautiful Soupを使う機会があったのでまとめておきました。

scraping-book.com

*1:軽く試した範囲では、lxmlはhtml.parserと同様の結果になり、html5libは推定されたエンコーディングの判別に失敗することがありました。html5libは先頭100バイトしかChardetに渡さないようです。html5libを使う場合は、「Content-Typeヘッダーの指定を尊重したい場合のコード」を使ったほうが良いかもしれません。

*2:requests/models.py参照

*3:requests/utils.py参照

*4:ISO-8859-1はRFC 2616で定められたデフォルト値です。RFC 7231ではこのデフォルト値はなくなったので、3.0に向けての議論はありますが、Kenneth Reitz氏はあまり乗り気ではなさそうです。

*5:詳しくはbs4/builder/_htmlparser.pybs4/dammit.pyを参照

*6:from_encodingに指定したエンコーディングで正しくデコードできない場合は無視されるので、文字化けしないこともあります。