日本語のデータを扱っていると、文字化けは今でも起きます。公的機関が配布するは Shift_JIS 系であることが珍しくなく、素直に読むと化けます。しかも化けたまま処理は進みます。
厄介なのは、化けたものを保存してしまうと元に戻せない場合があることです。読めなかった文字が「?」や別の記号に置き換わっていれば、そこから復元はできません。入口で決着をつけないと、後の工程では手遅れになります。
相手の申告を鵜呑みにしない
HTTP の応答には文字コードの申告が に入っていることがあります。ただしこの申告が間違っていることがあります。中身は Shift_JIS なのに UTF-8 と申告されている、あるいは申告そのものが無い。
requests は申告が無い場合に推測しますが、この推測も外れます。日本語のように候補が複数ある言語では、短いテキストだと判別が付きません。最終的には人が確かめるしかない場面が残ります。
確実にしたいなら、復号を自分で行うのが早いです。応答を生のまま受け取り、候補の文字コードを順に試します。が を返す場合は仕様上 UTF-8 と決まっているので悩む必要がありませんが、 のファイル配布では自分で決着をつけることになります。
一度どの文字コードだったか分かれば、以降は決め打ちにして構いません。毎回推測させるより、明示して外れたら例外にするほうが安全です。配信元が変えたときに、推測だと黙って別の解釈をしますが、明示していれば止まります。
CANDIDATES = ("utf-8-sig", "utf-8", "cp932", "euc-jp", "iso-2022-jp")
def decode_japanese(data: bytes) -> tuple[str, str]: """(復号した文字列, 使った文字コード) を返す。 cp932 は Shift_JIS の上位互換なので、Shift_JIS より先に試す。""" for enc in CANDIDATES: try: text = data.decode(enc) except UnicodeDecodeError: continue # 復号できても意味が通らないことがあるので、 # 日本語が含まれるはずのデータなら簡単な確認を入れる return text, enc raise UnicodeDecodeError("unknown", data, 0, 1, "候補のどれでも復号できない")読めない文字を黙って捨てたり置き換えたりする指定です。例外が出なくなるので一見解決したように見えますが、データは壊れています。しかも壊れた箇所を後から特定できません。例外が出るなら、出させてください。
cp932 と Shift_JIS は同じではない
Windows で作られた日本語のファイルは、正確には Shift_JIS ではなく cp932 であることがほとんどです。cp932 は Shift_JIS に文字を追加した拡張で、丸数字やローマ数字、一部の記号が含まれます。
Shift_JIS として読むと、これらの文字で例外になります。日本語のファイルを読むなら cp932 を先に試すのが実際的です。逆に cp932 で読めて Shift_JIS で読めないファイルは珍しくありません。
改行コードと BOM
文字コードと並んで問題になるのが改行です。Windows 由来のファイルは行末が CR+LF になっており、そのまま行を分割すると各行の末尾に CR が残ります。目に見えないので、値を比較したときだけ一致しない、という形で表に出ます。
先頭に付く BOM も同様です。UTF-8 の BOM 付きファイルを普通に読むと、1列目の列名の先頭に見えない文字が入ります。列名で参照している処理が、その列だけ見つけられなくなります。
| 症状 | 原因 | 対処 |
|---|---|---|
| 値の比較が一致しない | 行末の CR が残っている | 改行を正規化して読む |
| 1列目だけ参照できない | 先頭の BOM | utf-8-sig で読む |
| 数字が文字列になる | 全角数字・桁区切り | 取得直後に変換する |
| 「?」が混ざる | 誤った文字コードで読んだ | 取り直す。復元はできない |
import csvimport io
def read_csv_text(data: bytes) -> list[dict]: """BOM を取り除き、改行を揃えてから CSV として読む。""" text, enc = decode_japanese(data)
# BOM は utf-8-sig で復号すれば消えるが、他の文字コードでは残ることがある text = text.lstrip("\ufeff")
# newline="" を渡すのは、csv 側に改行の扱いを任せるため。 # 自分で splitlines すると、値の中にある改行まで行区切りにしてしまう buf = io.StringIO(text, newline="") return list(csv.DictReader(buf))上のコードで splitlines() を使っていないのは、値の中に改行が含まれている場合に壊れるからです。住所や備考の欄には改行が入っていることがあり、自分で行を分けると1件が2件に割れます。読み込みは専用の仕組みに任せてください。
受け取った生のバイト列をに残しておくと、文字コードの判断を間違えても取り直さずにやり直せます。復号したあとの文字列だけを保存すると、間違えた解釈が確定してしまいます。ここは EP.05 の「生のまま残す」がそのまま効く場面です。
文字コードの問題は、原因が分かってしまえば対処は単純です。しかし分かるまでが長い。取得直後に「どの文字コードで読んだか」を記録に残しておくと、後から追いやすくなります。次回は、そもそもスクレイピングに手を出してよいかの線引きを扱います。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。