Python Beautiful Soup gb2312 Windows-1252 文字化け問題

当時、Python のエンコーディング問題には本当に苦しめられ、いつか記事にまとめようとずっと思っていた。ここではまず、当時自分にとって最も役に立った 2 本の記事と重要な結論を整理しておく。今後 Beautiful Soup、gb2312gbkgb18030Windows-1252 の文字化けに遭遇したとき、すばやく原因を絞り込めるようにするためだ。

問題の現象

中国語の Web ページや RSS を取得すると、ページ側では gb2312gbk と宣言しているのに、解析後の中国語が文字化けすることがある。Beautiful Soup、feedparser、あるいは他のライブラリがエンコーディングを windows-1252 と判定してしまい、中国語本文がまったく読めなくなる場合もある。

よくある原因は、解析ライブラリが gb2312 をまったく認識できないことではなく、Web ページの実際の内容が厳密な gb2312 文字集合に収まっていないことだ。多くの中国語ページでは gb2312gbk が混用されており、場合によっては gb2312 と宣言しながら、本文にはすでに gb2312 の範囲を超える文字が含まれている。厳密に検査したとき、バイト列が gb2312 に適合しないと判断されると、パーサーが windows-1252 にフォールバックし、その結果として文字化けが発生する。

重要な結論

注意すべき点は、多くの Web ページが名乗っている gb2312 は、必ずしも本当に厳密な gb2312 ではないということだ。この種のページに遭遇したら、まず gb18030 でのデコードを試すとよい。

gb18030gbkgb2312 に対して下位互換性があり、カバー範囲も広い。Microsoft も gb2312gbk を、より広い中国語エンコーディングの扱いにマッピングすることが多い。これは一部の場面では便利だが、エンコーディング宣言と実際の内容が混ざり合いやすくなる原因にもなる。

つまり、ページには次のように書かれていても、

<meta charset="gb2312">

実際の内容は gb18030 として処理する必要がある可能性が高い。

Beautiful Soup での対処方法

Python 2 時代の書き方を使う場合は、Beautiful Soup に渡すときにエンコーディングを指定できる。

page = urllib2.build_opener().open(req).read()
soup = BeautifulSoup(page, fromEncoding="GB18030")

ここで重要なのは、HTML 内で宣言されている gb2312 を完全には信用せず、生のバイト列をより広い GB18030 としてパーサーに渡すことだ。

比較的新しい Beautiful Soup では、引数名は通常 from_encoding と書く。

from bs4 import BeautifulSoup

html = response.content
soup = BeautifulSoup(html, "html.parser", from_encoding="gb18030")

すでに手動でデコードしている場合は、先にバイト列を Unicode 文字列に変換してから Beautiful Soup に渡してもよい。

text = response.content.decode("gb18030", errors="replace")
soup = BeautifulSoup(text, "html.parser")

errors="replace" はデコードできないバイトを置換するため、取得時に本文をできるだけ残したい場合に向いている。デバッグ時にはあえてこれを使わず、例外を発生させて、どのバイト列が問題なのかを確認してもよい。

なぜ Windows-1252 と認識されるのか

典型例の一つが百度ニュース RSS だ。ページはエンコーディングを gb2312 と宣言しているが、実際の内容には gbk 範囲の文字が含まれていることがある。feedparser が gb2312 として厳密に検査すると、gb2312 の範囲を超える文字を検出し、gb2312 を諦めて windows-1252 にフォールバックする。すると中国語のバイト列が西欧文エンコーディングとして解釈され、最終的に文字化けして表示される。

したがって問題は必ずしも「ライブラリが賢くない」ことではなく、「Web ページが宣言しているエンコーディングと実際のバイト列が一致していない」ことにある。

調査手順

中国語ページの文字化けに遭遇したら、次の順序で確認するとよい。

  1. 生のバイト列を保持し、早い段階で .decode() しない。
  2. HTTP ヘッダーの Content-Type と HTML 内の meta charset を確認する。
  3. 宣言が gb2312 または gbk なのに解析に失敗する場合は、gb18030 を試す。
  4. 端末表示だけを見ず、repr() を出力するか、ファイルに書き出して確認する。
  5. 端末、エディター、データベース接続、出力ファイル自体が UTF-8 を使っているか確認する。

例:

raw = response.content

for encoding in ["utf-8", "gb18030", "gbk", "gb2312"]:
    try:
        text = raw.decode(encoding)
        print("decoded as", encoding)
        break
    except UnicodeDecodeError:
        pass

gb2312 が失敗して gb18030 が成功するなら、ページの宣言が十分に正確ではないことはほぼ説明できる。

参考記事

記事 1:Beautiful Soup gb2312 文字化け問題

記事 2:

まとめ

中国語 Web ページの文字化けを処理するとき、最も重要なのは「宣言されたエンコーディング」と「実際のエンコーディング」を区別することだ。多くの古い Web ページは gb2312 と書いていても、実際には gbk または gb18030 に近い。Beautiful Soup や feedparser のような解析ツールでは、Web ページの宣言を信じるより、直接 gb18030 を指定するほうが信頼できることが多い。

Leave a Reply