45395 - シコウサクゴ -

Shift_JIS文字化け追跡:HTTPレスポンスのencoding自動検出が失敗するとき

2026-04-16
AI駆動開発
AI駆動開発
Python
requests
エンコーディング
文字化け
API連携
Last updated:2026-04-21
8 Minutes
1416 Words

日本語のデータを返すAPIをrequestsで叩いたら、レスポンスが文字化けしました。原因はresponse.encodingがサーバーの指定と食い違っていたこと。

requestsライブラリのエンコーディング決定ロジックと、Shift_JIS/CP932混在環境での対処法をまとめます。

問題:response.textが文字化けする

症状

1
import requests
2
3
resp = requests.get("https://example.jp/api/items")
4
print(resp.text)
5
# => '繝ヲ繝シ繧カ繝シ繝ェ繧ケ繝' (意味不明な文字列)

期待していたのは「ユーザーリスト」。いわゆるUTF-8のバイト列をShift_JISとして解釈、あるいはその逆で起きる文字化けです。

調査

レスポンスヘッダと、requestsが判定したエンコーディングを確認します。

1
resp = requests.get("https://example.jp/api/items")
2
print("Content-Type:", resp.headers.get("Content-Type"))
3
print("encoding:", resp.encoding)
4
print("apparent_encoding:", resp.apparent_encoding)
5
print("先頭bytes:", resp.content[:100])

よくある出力例:

1
Content-Type: text/html
2
encoding: ISO-8859-1
3
apparent_encoding: Shift_JIS
4
先頭bytes: b'<!DOCTYPE html>\n<meta charset="Shift_JIS">\n...'

サーバーはShift_JISで返しているのに、requestsISO-8859-1と判定している。これが文字化けの原因です。

なぜrequestsはISO-8859-1と判定するのか

requestsは、Content-Typeヘッダにcharset=が含まれない場合、HTTP RFCに従って**ISO-8859-1をデフォルトにします**。

1
Content-Type: text/html → encoding = ISO-8859-1 (RFCデフォルト)
2
Content-Type: text/html; charset=UTF-8 → encoding = UTF-8
3
Content-Type: text/html; charset=Shift_JIS → encoding = Shift_JIS

問題は、多くのサーバーがcharsetを指定しないこと。その場合、レスポンスbody内の<meta charset="...">やBOMを見ないと正しいエンコーディングはわかりません。

解決策の段階

段階1: apparent_encodingに切り替える

1
resp = requests.get(url)
2
resp.encoding = resp.apparent_encoding
3
text = resp.text

apparent_encodingchardetライブラリがbodyの内容から推測したエンコーディング。多くのケースではこれで解決します。

注意:chardetは推測なので、短いレスポンスや混在したコンテンツでは誤判定します。英数字だけのレスポンスをISO-8859-1と判定することもあります。

段階2: Content-Typeとmetaを自分で解析

より堅牢にするなら、ヘッダ→body内のmeta→apparent_encodingの優先順で判定します。

1
import re
2
3
def detect_encoding(resp: requests.Response) -> str:
4
# 1. Content-Typeヘッダのcharset
5
ct = resp.headers.get("Content-Type", "")
6
m = re.search(r"charset=([\w-]+)", ct)
7
if m:
8
return m.group(1)
9
10
# 2. body内の <meta charset="...">
11
body_head = resp.content[:2048]
12
m = re.search(
13
rb'<meta[^>]+charset=["\']?([\w-]+)',
14
body_head, re.IGNORECASE
15
)
14 collapsed lines
16
if m:
17
return m.group(1).decode("ascii", errors="ignore")
18
19
# 3. BOMで判定
20
if resp.content.startswith(b"\xef\xbb\xbf"):
21
return "utf-8"
22
if resp.content.startswith(b"\xff\xfe"):
23
return "utf-16-le"
24
25
# 4. chardetに委ねる
26
return resp.apparent_encoding or "utf-8"
27
28
resp = requests.get(url)
29
resp.encoding = detect_encoding(resp)

段階3: 既知のサイトはドメイン別にハードコード

相手が固定なら、推測せずに決め打ちが最も確実です。

1
DOMAIN_ENCODING = {
2
"example.jp": "shift_jis",
3
"legacy.co.jp": "cp932",
4
"modern.com": "utf-8",
5
}
6
7
def fetch(url: str) -> str:
8
from urllib.parse import urlparse
9
host = urlparse(url).netloc
10
encoding = DOMAIN_ENCODING.get(host)
11
12
resp = requests.get(url)
13
if encoding:
14
resp.encoding = encoding
15
else:
2 collapsed lines
16
resp.encoding = detect_encoding(resp)
17
return resp.text

Shift_JIS vs CP932 の落とし穴

**Shift_JISと名乗っているサーバーの大半は、実はCP932(Windows拡張)**を使っています。

1
# 標準Shift_JISでは扱えない文字
2
text = "①②③" # 丸数字はCP932独自の拡張領域
3
4
resp.encoding = "shift_jis"
5
resp.text # → UnicodeDecodeError または文字化け
6
7
resp.encoding = "cp932"
8
resp.text # → 正常にデコード
名前範囲使い分け
shift_jisJIS X 0208の範囲厳密な規格データ
cp932 / ms932Shift_JIS + Windows拡張(丸数字、ローマ数字、機種依存文字)実務では基本こちら
shift_jisx0213JIS X 0213(2004年改訂)人名・地名の特殊字

迷ったらcp932を使うのが安全です。Shift_JISで読めるデータは全てCP932でも読めます(上位互換)。

1
# 堅牢な置き換え
2
resp.encoding = "cp932" if "shift" in resp.encoding.lower() else resp.encoding

実際に遭遇した事故

事故1: errors=“strict”で例外が出て止まる

1
resp.encoding = "shift_jis"
2
text = resp.text # UnicodeDecodeError: 丸数字を読めない

CP932独自文字が含まれていると、厳密なShift_JISでは落ちます。

対処:

1
# 1) CP932に切り替える
2
resp.encoding = "cp932"
3
4
# 2) それでもダメなら errors="replace" で不正バイトを置換
5
decoded = resp.content.decode("cp932", errors="replace")

事故2: Content-Typeに嘘が書かれている

1
Content-Type: text/html; charset=UTF-8
2
(実際のbodyは Shift_JIS)

サーバー側のバグや設定ミスで、ヘッダと実体が食い違う。この場合、ヘッダを信じると文字化けします。

対処:ヘッダのcharsetを信じる前に、bodyのmetachardetの結果と突き合わせます

1
def cross_check_encoding(resp):
2
header_enc = resp.encoding
3
body_enc = resp.apparent_encoding
4
5
if header_enc and body_enc and header_enc.lower() != body_enc.lower():
6
logger.warning(
7
f"エンコーディング不一致: header={header_enc}, body={body_enc}"
8
)
9
# 一般には body を優先した方が安全
10
return body_enc
11
return header_enc or body_enc or "utf-8"

事故3: BOM付きUTF-8の最初の文字が化ける

1
text = resp.text
2
print(text[0]) # => '\ufeff' (BOMが残っている)

requestsはUTF-8のBOM(\xef\xbb\xbf)を自動除去しません。先頭に\ufeffが残り、JSON解析で落ちたりします。

対処:

1
text = resp.text
2
if text.startswith("\ufeff"):
3
text = text[1:]

または encoding="utf-8-sig" を指定(BOMを自動的に取り除く)。

1
resp.encoding = "utf-8-sig" if resp.content.startswith(b"\xef\xbb\xbf") else "utf-8"

テスト戦略

文字化けバグは「実環境では出るがユニットテストでは出ない」になりがちです。実データの先頭バイト列を固定したテストを書きます。

1
def test_decode_shift_jis_response():
2
mock_resp = mock.Mock()
3
mock_resp.headers = {"Content-Type": "text/html"}
4
mock_resp.content = "ユーザーリスト".encode("cp932")
5
mock_resp.apparent_encoding = "shift_jis"
6
7
encoding = detect_encoding(mock_resp)
8
assert encoding.lower() in ("cp932", "shift_jis")

過去に事故ったサンプルをtests/fixtures/に置いて回帰テストにしておきます。

まとめ

症状原因対処
ISO-8859-1と判定されるContent-Typecharsetがないapparent_encodingに切り替える
Shift_JISで丸数字が化けるCP932拡張文字cp932を指定
ヘッダはUTF-8なのに文字化けサーバー設定ミスbodyのmeta/chardetと突き合わせる
先頭が\ufeffUTF-8 BOMutf-8-sigまたは手動除去

教訓: 日本語API連携では、response.textをそのまま信じません。必ずencodingを明示的に設定します。ドメインが固定なら、決め打ちが最も事故が少ないです。

Pythonの標準HTTPライブラリは国際化対応が完全ではありません。特に日本語のレガシーAPIとの連携では、エンコーディングは自分でコントロールするというスタンスが必要です。

Article title:Shift_JIS文字化け追跡:HTTPレスポンスのencoding自動検出が失敗するとき
Article author:45395
Release time:2026-04-16

記事へのご質問・ご感想をお聞かせください

フィードバックを送る