誰かから受け取ったファイルをPythonで開いたら、画面に見覚えのない記号がずらりと並んだ。あるいは、赤い文字でUnicodeDecodeErrorと表示されて止まった。
そんな経験はありませんか。
自分のパソコンでは動いたのに、渡した相手の環境では動かない。文字コードのトラブルは、たいていこの形でやってきます。
やっかいなのは、コードの書き方が間違っているわけではない点です。読み方の指定が足りないだけなのに、原因がとても見えにくいのです。
この記事では、文字コードの仕組みと、文字化けやエラーが出たときの直し方を順番に整理していきます。
文字コードは、文字と番号の対応表¶
まずは正体から確認しましょう。コンピュータが保存できるのは数値だけで、文字そのものを直接は書き込めません。
そこで、文字ごとに番号を割り当てた対応表を用意します。これが文字コードです。
保存するときは、その表を見て番号を書き込む。読むときは、同じ表を見て文字に戻す。
書くときと読むときで同じ表を使えば、文字はきれいに往復します。違う表を使うと、そこで話がずれます。
日本語まわりでよく登場するのは、次の3つです。
| 名前 | 特徴 | よく見かける場所 |
|---|---|---|
| UTF-8 | 世界中の文字を扱える現在の標準 | Web、Linux、macOS、Python |
| cp932(Shift_JIS) | Windowsの日本語環境で長く使われてきた | 古いExcel、社内システムの出力 |
| EUC-JP | かつてUNIX系で使われていた | 古いWebページや古いメール |
これから自分で作るファイルは、迷わずUTF-8にして構いません。困るのは、たいてい誰かから受け取った古いファイルを開くときです。
UnicodeとUTF-8は同じものではない¶
ここは初心者がよく混乱するところなので、先に整理しておきます。
Unicodeは、世界中の文字に番号を振った巨大な台帳です。番号の一覧そのもの、と考えてください。
UTF-8は、その番号をどんなバイトの並びで書き出すかを決めた方式です。台帳がUnicodeで、その書き方の作法がUTF-8という関係になります。
だから、Unicodeで保存しますという言い方は、実は少し足りません。どの方式で書くかまで決めて、はじめてファイルになります。
Pythonのstrとbytesは、別の生き物¶
Python 3では、文字を扱う型とバイトを扱う型がはっきり分かれています。この区別が、文字コードを理解する土台になります。
str は人間が読む文字列です。bytes は、その文字列を何らかの文字コードで書き出したあとのバイトの並びです。
同じ言葉でも、どちらの姿でいるかで長さが変わります。実際に確かめてみましょう。
s = "こんにちは"
print(len(s)) # 5 … 文字の数
utf8 = s.encode("utf-8")
print(len(utf8)) # 15 … UTF-8では1文字3バイト
sjis = s.encode("cp932")
print(len(sjis)) # 10 … cp932では1文字2バイト
print(utf8.decode("utf-8")) # こんにちは
同じ5文字が、UTF-8では15バイト、cp932では10バイトになりました。人間から見た文字数と、ファイルの大きさは別物なのです。
encodeとdecodeは、往復の切符¶
方向を取り違えると混乱するので、言葉の意味を固定しておきましょう。
encode は、文字列をバイトに変える方向です。書き出すとき、送り出すときに使います。
decode は、バイトを文字列に戻す方向です。読み込むとき、受け取るときに使います。
| メソッド | 変換の向き | いつ使うか |
|---|---|---|
str.encode(...) |
str → bytes |
ファイルに書く、ネットワークへ送る |
bytes.decode(...) |
bytes → str |
ファイルを読む、レスポンスを受け取る |
覚え方はかんたんです。人間が読むのがstr、機械に渡すのがbytesで、渡す側の変換がencodeです。
Webからデータを取ってくるときも、この往復が裏側で起きています。【関連記事】Pythonのrequestsとは?WebからデータをとってくるHTTP通信の基本を初心者向けに解説
文字化けは、読むときの表を間違えた結果¶
ここから本題です。文字化けは、バイトが壊れて起きるわけではありません。
書いたときと違う表で読んだから、別の文字として解釈されただけです。バイト自体は無事なことがほとんどです。
たとえばUTF-8で書かれた こんにちは を、cp932だと思い込んで読むとこうなります。
data = "こんにちは".encode("utf-8")
print(data.decode("cp932", errors="replace"))
# 縺薙s縺ォ縺。縺ッ
見覚えのある並びではないでしょうか。縺 や s が混ざった文字列が出たら、UTF-8のファイルをcp932として読んでいる可能性が高いと考えてください。
化け方には傾向があるので、目印として覚えておくと調査が速くなります。
| 画面に出るもの | 疑うべき状況 |
|---|---|
| 縺、繧、繝、譁 が並ぶ | UTF-8のデータをcp932で読んでいる |
| 黒いひし形の記号が点々と混ざる | cp932のデータをUTF-8で読んでいる |
| ニ や ヒ など半角カナが混ざる | EUC-JPなど別の文字コードで読んでいる |
| 一部の漢字や絵文字だけ ? になる | 書き出す側で扱えず置き換えられた |
| 先頭にだけ余計な記号が付く | BOMを読み飛ばしていない |
私は10年ほどエンジニアとして開発に関わってきましたが、この対応表を頭に入れてからは調査時間が目に見えて短くなりました。化けた文字の形そのものが、読み方を教えてくれる手がかりになるからです。
UnicodeDecodeErrorが出たときの直し方¶
文字化けよりも分かりやすい形で失敗することもあります。それがUnicodeDecodeErrorです。
これは、指定した表ではどうしても解釈できないバイトに出会ったという合図になります。
data = "こんにちは".encode("cp932")
data.decode("utf-8")
# UnicodeDecodeError: 'utf-8' codec can't decode byte 0x82
# in position 0: invalid start byte
エラーメッセージには、行き詰まったバイトの値と場所まで書かれています。position 0 と出ているので、先頭から合っていないと分かります。
正しい直し方は、読み方を実際の中身に合わせることです。この例なら data.decode("cp932") で解決します。
つまり、エラーが出たときにまず疑うのは、ファイルの中身ではなく自分が指定した文字コードのほうです。
errors引数は、逃げ道であって解決ではない¶
decode には errors という引数があり、失敗したバイトの扱いを変えられます。検索するとよく出てくるので、意味を正しく知っておきましょう。
| 指定 | 動き |
|---|---|
strict(既定) |
読めないバイトがあれば例外を出して止まる |
replace |
読めない部分を代替文字に置き換えて続行する |
ignore |
読めない部分を捨てて続行する |
ignore は一見おだやかですが、実際は情報が消えます。先ほどの5文字を ignore で無理に読ませると、残ったのはたった1文字でした。
エラーは消えたのにデータが欠けている。この状態がいちばん危険で、後日になって気づくことになります。
errorsを使ってよいのは、中身が多少欠けても困らないログの調査などに限ると考えてください。 本番のデータ処理では、正しい文字コードを突き止めるのが先です。
open()でencodingを省略してはいけない理由¶
ここが、実務でいちばん事故が起きるポイントです。ファイルを開くとき、encoding を書かない人はとても多いのです。
# 環境によって結果が変わる書き方
with open("data.txt") as f:
text = f.read()
# どこで動かしても同じ結果になる書き方
with open("data.txt", encoding="utf-8") as f:
text = f.read()
encoding を省略すると、Pythonはその環境のロケール設定に従います。macOSやLinuxではUTF-8になることが多い一方、日本語版Windowsではcp932が選ばれてきました。
つまり、自分の手元で動いたコードが、Windowsの同僚の環境だけで壊れる。この現象の正体がこれです。
見落としを機械に見つけてもらう方法もあります。Python 3.10で追加されたEncodingWarningを有効にすると、encoding を書き忘れた場所を警告してくれます。
python -X warn_default_encoding your_script.py
実行すると、該当行に対して encoding argument not specified という警告が出ます。既存のスクリプトの点検に、そのまま使える機能です。
ファイルの開き方そのものを復習したいときは、こちらもあわせてどうぞ。【関連記事】Pythonのファイル読み書きとは?open()とwith文の基本を初心者向けに解説
Python 3.15からUTF-8が既定になる¶
ここは最新の話題です。この省略時の揺れをなくす変更が、いよいよ入ります。
PEP 686という提案がFinalとして受理され、Python 3.15からUTF-8モードが既定で有効になります。ファイルや標準入出力の既定の文字コードが、どのOSでもUTF-8にそろう変更です。
Python 3.15の正式リリースは2026年10月1日が予定されています。つまり、あと少しで新しい既定が現実のものになります。
古いスクリプトが動かなくなる場合に備えて、逃げ道も用意されています。環境変数 PYTHONUTF8=0 を設定するか、-X utf8=0 を付ければ、これまでどおりの動きに戻せます。
では、いまから encoding を書くのはむだになるのでしょうか。答えは逆です。
しばらくは3.15より古いPythonが現場に残りますし、明示された指定は読む人にも意図が伝わります。encoding="utf-8" は、これからも書き続けてよい一行です。
3.15のほかの変更点も気になる方は、こちらでまとめています。【関連記事】Python 3.15の新機能を先取り解説!lazy importやfrozendictで何が変わるのか
ExcelでCSVが化けるときは、BOM付きで書く¶
現場でとくに多い相談が、これです。PythonでUTF-8のCSVを作ったのに、Excelで開くと日本語が化ける。
原因は、Excelが文字コードを推測しきれないところにあります。何もヒントがないと、Windows既定の読み方をしてしまうのです。
そこで使うのが utf-8-sig という指定です。ファイルの先頭に、UTF-8ですという小さな目印を付けてくれます。
import csv
rows = [["商品名", "個数"], ["みかん", 3]]
with open("out.csv", "w", newline="", encoding="utf-8-sig") as f:
csv.writer(f).writerows(rows)
この目印はBOMと呼ばれ、正体は先頭の3バイトです。読み込むときも utf-8-sig を指定すれば、Pythonがその3バイトを自動で読み飛ばしてくれます。
逆に、Web APIやプログラム同士のやりとりでは、BOMは付けないほうが無難です。相手のプログラムが先頭の3バイトをデータとみなして誤動作することがあります。
Excelに渡すファイルだけ utf-8-sig。それ以外はふつうの utf-8。この使い分けで、まず困りません。
CSVそのものの扱い方は、別記事で詳しく整理しています。【関連記事】Pythonのcsvモジュールとは?表データの読み書きと文字化けの防ぎ方を初心者向けに解説
今日から使える形にまとめておく¶
最後に、手を動かすときの判断だけに絞ります。
新しく作るファイルはUTF-8にする。open() には必ず encoding を書く。この2つを習慣にするだけで、トラブルの大半は起きなくなります。
受け取ったファイルが読めないときは、まず自分の指定した文字コードを疑ってください。日本語のファイルなら、utf-8 の次に cp932 を試すのが近道です。
そして、errors="ignore" で黙らせないこと。エラーは、読み方が違うと教えてくれている親切な相手です。
まずは手元のスクリプトを開いて、encoding を書き忘れた open() が無いか探すところから始めてみましょう。読み込んだあとの文字列を整える方法も知っておくと、データの下ごしらえがぐっと楽になります。【関連記事】Pythonの文字列メソッドとは?strip・split・replaceでデータを整える基本を初心者向けに解説
ここまでお読みいただきありがとうございました。