ブラウザーでは日本語が読めるのに、Pythonで取り出したタイトルだけが文字化けすることがあります。取得したバイト列、文字列への変換、保存したファイルの表示という三つの段階を分けて調べると、設定を変える場所が見えてきます。
文字コードを推測して上書きする前に、HTTPヘッダーとHTML内の指定を確認します。UTF-8でないページへ一律にUTF-8を指定しても、解決にはなりません。
contentとtextの違い
Requestsのresponse.contentは、応答本文のバイト列です。response.textは、Requestsが選んだ文字コードでそのバイト列をデコードした文字列です。使用する文字コードはresponse.encodingで確認・変更できます。
HTTPヘッダーに適切なcharsetがない場合など、HTMLの中にある指定とRequestsが選ぶ値が食い違うことがあります。BeautifulSoupへバイト列を渡すと、HTML内の指定を含めて文字コードを推測できます。ただし推測が常に正しいとは限りません。
架空のUTF-8ページで確認する
次のencoding-example.htmlをUTF-8で保存します。ここではHTMLのmeta要素にUTF-8の指定があることが、入力の前提です。
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>架空の請求資料</title></head>
<body><p>金額を確認します。</p></body>
</html>
練習用フォルダーでHTTPサーバーを起動し、別のターミナルからPythonを動かします。サーバーのContent-Typeにcharsetが付かない条件を想定した例です。
py -m http.server 8000 --bind 127.0.0.1
from pathlib import Path
import requests
from bs4 import BeautifulSoup
response = requests.get("http://127.0.0.1:8000/encoding-example.html", timeout=(3, 10))
response.raise_for_status()
print("HTTP:", response.headers.get("Content-Type"))
print("Requests:", response.encoding)
soup = BeautifulSoup(response.content, "html.parser")
print("BeautifulSoup:", soup.original_encoding)
print("title:", soup.title.get_text() if soup.title else "なし")
# この入力は自分でUTF-8として保存したHTMLです。
response.encoding = "utf-8"
with Path("decoded.html").open("x", encoding="utf-8") as f:
f.write(response.text)
タイトルに「架空の請求資料」が出るかを見ます。Requestsの表示値はサーバーの応答によって変わるため、期待するコード名だけを固定して成功と判定しません。出力先のdecoded.htmlは、新規作成で保存します。
この例でresponse.encodingをUTF-8にする根拠は、自分で作った入力の保存形式です。実サイトなら、ヘッダー、meta要素、配布元の仕様、読めるはずの文字を照合して選びます。soup.original_encodingの結果を無条件に正解とする処理にもしていません。
どこで文字が変わったかを追う
取得直後のHTMLでは読めていて、CSVやテキストファイルだけがおかしい場合は、保存時と開く側の文字コードを確認します。保存形式をUTF-8にしただけで、開く側が同じ形式を使うとは限りません。
反対に、デコードした時点で文字が置換記号になっていれば、保存方法だけ変えても元の文字には戻りません。取得したバイト列を残せる条件なら、そこへ戻って変換をやり直します。errors="ignore"で読めない部分を捨てる方法は、原因を隠すことがあるため、文字を正しく取得できた確認の代わりにはなりません。
文字化けに見えても、端末のフォントや出力の表示設定の問題という場合もあります。同じ文字列をUTF-8で保存して別の表示手段で確認し、取得・デコード・保存・表示のどこから違うかを順に絞ります。
関連する作業
HTML取得の最小例と、CSVへ保存するときの文字コードは次の記事に続きます。
