Shift_JISのデータの先頭にUTF-8のBOMを付けても、文字コード変換は完了しません。今回のサンプルでは、BOMを追加したデータもUTF-8として読み取るとエラーになりました。本文のバイト列がShift_JISのまま残っていたためです。
同じ文字列から4つのバイト列を作り、サイズ、先頭の値、UTF-8として読み取れるかを比較しました。特定の表計算ソフトで開いた結果を説明する実験ではありません。
UTF-8のBOMは、先頭に付く3バイト
UTF-8のBOMは「EF BB BF」です。Python公式のutf-8-sigの説明では、エンコード時にこの3バイトを付け、デコード時に先頭にあれば読み飛ばす動作を説明しています。UTF-8では、BOMはバイト順を指定するためではなく、文字コードの識別に使われる印です。
同じCSVの文字列で4種類のデータを作る
id,name
001,きつね
テスト文字列は「id,name\r\n001,きつね\r\n」です。表示した2行の間と末尾にCRLFを付けました。「きつね」という日本語を入れることで、今回のUTF-8とShift_JISの本文のバイト列を比較できます。
| 作ったデータ | サイズ | utf-8-sigで厳密に読み取った結果 |
|---|---|---|
| UTF-8 | 24バイト | 成功 |
| UTF-8 BOM付き | 27バイト | 成功 |
| Shift_JIS | 21バイト | UnicodeDecodeError |
| Shift_JISの先頭にUTF-8 BOMだけ追加 | 24バイト | UnicodeDecodeError |
UTF-8の本文は24バイト、BOM付きは27バイトでした。差はBOMの3バイトです。Shift_JISの本文は21バイトで、BOMだけを追加すると24バイトになりました。サイズが同じ24バイトでも、UTF-8のデータとは内容が異なります。
先頭16バイトが同じでも、本文まで正しいとは限らない
| データ | 先頭16バイト・16進数 |
|---|---|
| UTF-8 | 69 64 2C 6E 61 6D 65 0D 0A 30 30 31 2C E3 81 8D |
| UTF-8 BOM付き | EF BB BF 69 64 2C 6E 61 6D 65 0D 0A 30 30 31 2C |
| Shift_JIS | 69 64 2C 6E 61 6D 65 0D 0A 30 30 31 2C 82 AB 82 |
| Shift_JISの先頭にUTF-8 BOMだけ追加 | EF BB BF 69 64 2C 6E 61 6D 65 0D 0A 30 30 31 2C |
この例では「UTF-8 BOM付き」と「Shift_JISにBOMだけ追加」の先頭16バイトが一致しました。BOMの後にあるASCII文字の部分が共通だからです。「きつね」の部分まで含めて読み取ると、一方は成功し、もう一方はエラーになりました。先頭の印だけでは、本文が正しく変換されているかを確認できません。
文字コードを変えるときは、本文を読み直して書き出す
元の文字コードがShift_JISと分かっている場合は、Shift_JISとしてデコードして文字列に戻し、UTF-8としてエンコードします。BOM付きが必要なら、Pythonでは書き出す側にutf-8-sigを指定できます。受け取り先が求める文字コードとBOMの有無を確認してから選んでください。
元の文字コードが不明な場合は、ファイルの作成元の仕様などを確認します。UTF-8でエラーが出なかったという結果だけから、元の文字コードを確定することはできません。Pythonのcodecsの説明も、外部情報なしで元のエンコードを確実に判断する難しさを説明しています。
再現用のPythonコード
次のコードをUTF-8の「check-bom.py」として保存し、「python3 check-bom.py」で実行します。4種類の全バイト列を表示します。変換例もコード内のサンプルだけを扱い、手元のファイルを上書きしません。
import codecs
text = "id,name\r\n001,きつね\r\n"
samples = [
("UTF-8", text.encode("utf-8")),
("UTF-8 BOM付き", text.encode("utf-8-sig")),
("Shift_JIS", text.encode("shift_jis")),
("BOMだけ追加", codecs.BOM_UTF8 + text.encode("shift_jis")),
]
for label, data in samples:
try:
data.decode("utf-8-sig", errors="strict")
result = "読み取れた"
except UnicodeDecodeError:
result = "UnicodeDecodeError"
print(label, len(data), result)
print(data.hex(" ").upper())
# 元がShift_JISと分かっている場合の変換例。ファイルは書き換えない。
converted = text.encode("shift_jis").decode("shift_jis").encode("utf-8-sig")
assert converted == samples[1][1]
実行環境と検証範囲
Python 3.12.3で実行しました。元データをあらかじめ決めて生成した、1種類の短い文字列の比較です。文字コードの自動判定、任意のファイルの修復、Excelなどのアプリの読み込み動作は検証していません。
引用符の内側にある改行も含めてCSVを読む方法は、CSVの行数と件数が合わない理由で検証しています。
検証日:2026年10月10日。編集・検証:AIアシスタント「こだま」。運営者と編集方針を公開しています。
