チェック・トラブル対処Read this article in English

文字化けの直し方 — 「縺ゅ↑縺溘」の解読と元のテキストに戻す手順

文字化けはデコードのズレであって、ディスク上のバイト列はほぼ確実に書き込まれたときのままです。読み返したプログラムが間違ったエンコーディングを指定しただけなので、直し方はたいていコマンド1つで済みます。作業の本体は、どの2つのエンコーディングを取り違えたかを突き止めるところです。

この記事では、その特定作業を順に進めます。対照表・判別ルール・復元コマンド・戻らないケースの順に進み、出てくる文字列とバイト値はすべて手元で再実行できるコマンドの出力です。

誤読ペア8通りの対照表 — 自分の化け方を突き合わせる

エンコードはPython標準ライブラリで行いました。8ペアのうち6ペアは、Pythonと手元の2つのNodeで同じ結果でした。一致しないのはWindows-1252の2行とEUC-JPの行です。Windows-1252の値はWHATWG windows-1252索引に従い、再現できるのはNode 24.18.1と、日本語の行についてはPythonのcp1252です。Node 22.13.0は80〜9Fをすべて素のC1制御文字にするため、どちらの行も再現しません。EUC-JPの行はPythonが漢字をもう1文字復元します。定義の無いバイトでも両者は分かれ、Pythonは置換文字、WHATWG準拠のデコーダーはこんにちはの行に注記した制御文字を出します。置換文字の個数は手がかりであって証拠にはなりません。

  • 日本語のUTF-8バイト列9バイトは9Eで終わり、CP932はこれを2バイト文字の1バイト目として扱うため、続きがないと判断して最後の1バイトだけを捨てます。これが「譌・譛ャ隱」の末尾の置換文字で、ファイルが壊れた印ではなく文字の途中でバイト列が終わった印です。
  • caféはCP932にエンコードできず、Pythonはéの時点でUnicodeEncodeErrorを投げます。Shift_JISで書き出す経路にアクセント付きラテン文字を通す手段は無く、欧州言語をその経路に流す設計は成立しません。
サンプル3種を1回エンコードし、8通りの間違った指定でデコードした結果。

UTF-8のバイト列をCP932 / Shift_JISとして読む
  こんにちは  ->  縺薙s縺ォ縺。縺ッ
  日本語      ->  譌・譛ャ隱�
  café        ->  cafテゥ

UTF-8のバイト列をWindows-1252として読む
  日本語      ->  日本語
  café        ->  café
  こんにちは  ->  ã“ã‚“ã«ã¡ã¯     (見えないU+0081の制御文字4個はここでは省いた)

BOM付きUTF-8をWindows-1252として読む
  café        ->  café

UTF-8のバイト列をGBKとして読む
  日本語      ->  鏃ユ湰瑾�

UTF-8のバイト列をEUC-JPとして読む
  日本語      ->  �ユ��        (ほかに見えない制御文字3個。Pythonとは結果が違う)

CP932のバイト列をUTF-8として読む
  こんにちは  ->  ����ɂ���
  日本語      ->  ���{��

EUC-JPのバイト列をCP932として読む
  こんにちは  ->  、ウ、ヒ、チ、マ       (2つめの、の後に私用領域の文字が隠れている。ここでは省いた)

BOM付きUTF-16LEをUTF-8として読む
  こんにちは  ->  ��S0�0k0a0o0
  café        ->  ��c a f �      (空きに見えるのはNULバイト)

各行の再現方法:
  python3 -c "print('こんにちは'.encode('utf-8').decode('cp932', errors='replace'))"
  python3 -c "print('日本語'.encode('utf-8').decode('cp1252'))"
  node -e "process.stdout.write(new TextDecoder('euc-jp').decode(new TextEncoder().encode('日本語')))"

見た目から誤読ペアを当てる判別ルール

化けた1行から、ファイルを触る前に原因を絞れます。

  • 2〜3文字おきに縺・繧・繝が繰り返される: UTF-8をShift_JISまたはCP932として読んだ場合です。ひらがなはUTF-8の先頭2バイトがE3 81かE3 82で共通し、CP932はE3 81を縺に当てます。区切りがその組に当たるたび同じ漢字が出るので、「縺薙s縺ォ縺。縺ッ」では縺が4回並びます。
  • æ・è・ãの連続に、曲がった引用符やダッシュ、Âが混じる: UTF-8を1バイト系の欧文エンコーディングとして読んだ場合です。日本語の先頭バイトはE6やE8なのでæとèが出ます。アクセント付きラテン文字ならcaféやäöのようなÃのペアです。
  • 1行目の先頭だけにが付き、他の行は正常: UTF-8のBOM(EF BB BF)を1バイト系として読んだ場合です。本文は無事なことも多く、余計なヘッダー行が混ざったと誤診しやすいです。
  • ほぼ置換文字だらけで、ASCIIの記号だけがときどき生き残る: 逆方向で、従来の2バイト系ファイルをUTF-8として読んでいます。生き残るのは2バイト目がASCII範囲だったバイトで、日本語のCP932は93 FA 96 7B 8C EA、この7Bが「���{��」の中の{です。
  • 、 ウ ヒ チ マのような半角カタカナがほぼすべてを占める: EUC-JPのファイルをShift_JISとして読んだ場合です。EUC-JPは漢字とかなを2バイトともA1〜FEで表し(ASCIIは1バイトのまま、半角カタカナは8Eが前に付く)、Shift_JISはA1〜DFを1バイトの半角カタカナに当てているので、大半が1バイトずつカタカナになります。DFを超えるバイトだけは2バイト文字の1バイト目とみなされ、次の1バイトを飲み込みます。
  • 文字が1文字ずつ空白で区切られて見える: UTF-16をUTF-8として読んでいます。ASCIIなら各文字の2バイト目がNULです。日本語ではU+30xxの上位バイトが0x30なので、化けた文字のあとに実在する0が見えます。表のS0・k0・a0がそれです。
  • 見慣れない中国語の漢字が密集する: UTF-8をGBKとして読んだ場合です。元が漢字なら鏃や瑾、ひらがなが多い文なら銇が繰り返されます。原因はCP932のケースと同じで、参照される文字表が違うだけです。
  • æ—¥のような3文字の塊になっている: 化けが2回起きています。化けた状態でUTF-8として保存され、もう一度間違った指定で読まれた状態で、二重エンコードされたUTF-8の見分け方のとおり復元も2回に分けます。
  • 一部の文字だけが四角い箱や空白になり、他は正常: 文字化けではありません。デコードは成功していて、フォントにその字のグリフが無いだけで、対処法も別です(豆腐(□)とグリフ欠落)。

iconvで元に戻す手順とエンコーディング名の罠

ペアが分かれば復元は機械的です。元のファイルが残っているなら、正しい指定でデコードし直すだけです。化けた文字列しか残っていないなら、化けの原因になったコーデックでエンコードし直すと元のバイト列が再現されるので、それを正しい指定で読みます。

落とし穴はエンコーディング名です。iconvのCP932とSHIFT_JISは別のコーデックでエイリアスではないため、片方で通る復元がもう片方では中断します。CP1252とISO-8859-1も同様で、文字化けが80〜9Fの範囲に置く文字はWindows-1252には存在しますがISO-8859-1にはありません。

  • Windows由来のファイルや日本語ロケールの表計算ソフトの書き出しはCP932を指定します。CP932はShift_JISのMicrosoft拡張で、上の罠1のとおり厳密なShift_JISが受け付けない文字を含みます(Shift_JISとCP932の落とし穴)。
  • 上のISO-8859-1の例では、iconvは終了ステータス0で間違ったバイト列を出しました。emダッシュをハイフン(2D)に、œをoe(6F 65)に、žをz(7A)に置き換えています。成功終了は復元できた証拠にならないので、戻したテキストは必ず目で確認してください。
  • Nodeはデコードはできてもエンコードはできません。TextDecoderはshift_jis・euc-jp・windows-1252・gbkなどを扱い、そのshift_jisはWHATWG Encoding Standardに従うのでCP932の追加文字も読めます(87 40は①)。一方TextEncoderは引数を無視して常にUTF-8を出力します。shift_jisを渡してもencodingはutf-8です。
  • Buffer.from(text, 'latin1')は欧文の文字化けを戻す定番の1行ですが、効くのはLatin-1の範囲だけです。caféはcaféに戻りますが、日本語ではU+2014のemダッシュがバイト14に切り詰められて壊れるので、Windows-1252のケースはiconvを使ってください。
# 元のファイルが残っている場合: 正しい指定でデコードするだけ。
iconv -f CP932 -t UTF-8 original.csv > fixed.csv

# 化けた文字列しかない場合: 化けの原因になったコーデックでエンコードし直す。
# UTF-8をCP932として読んでしまったケース。fixed.csvは正しいUTF-8で出てくる。
iconv -f UTF-8 -t CP932  garbled.csv > fixed.csv
# UTF-8をWindows-1252として読んでしまったケース。
iconv -f UTF-8 -t CP1252 garbled.csv > fixed.csv

# 罠1: CP932とSHIFT_JISは別物。
printf '~' | iconv -f UTF-8 -t CP932     | xxd -p   # 8160
printf '~' | iconv -f UTF-8 -t SHIFT_JIS            # iconv: iconv(): Illegal byte sequence
printf '〜' | iconv -f UTF-8 -t CP932                # iconv: iconv(): Illegal byte sequence
printf '〜' | iconv -f UTF-8 -t SHIFT_JIS | xxd -p   # 8160
printf '①' | iconv -f UTF-8 -t CP932     | xxd -p   # 8740
printf '①' | iconv -f UTF-8 -t SHIFT_JIS            # iconv: iconv(): Illegal byte sequence

# 罠2: CP1252なら戻るものが、ISO-8859-1では黙って壊れる。
printf '日本語' | iconv -f UTF-8 -t CP1252     | xxd -p  # E697A5E69CACE8AA9E -> 日本語
printf '日本語' | iconv -f UTF-8 -t ISO-8859-1 | xxd -p  # E62DA5E66F65ACE8AA7A、終了ステータスは0

本当に戻らない2つのケース

復元が成立するのは、元のバイト列がまだどこかに残っているからです。

1つめは置換文字です。デコーダーが解釈できないバイト列に当たるとU+FFFDを出力しますが、この文字は何を置き換えたかを覚えていません。再エンコードすると疑問符になるか失敗し、情報は消えます。2つめは上書き保存です。化けたファイルをUTF-8として書き出し直すと、残るのは「間違った文字」を正しくエンコードしたファイルです。バイト列はどこも不正でないので、どのツールも間違いと判定できません。

文字化けを見つけたら、エディタで開いて保存しないこと、書き戻す取り込み処理を走らせないこと。まず生のバイト列をコピーして退避します。すでに保存し直されていたら、復元元は目の前のファイルではなく上流です。翻訳者からの納品物、直前のコミット、データベースのダンプを当たってください。

# すでに1バイトがU+FFFDになっている。再エンコードしても戻らない。
python3 -c "
u = '日本語'.encode('utf-8').decode('shift_jis', errors='replace')
print(u)                                    # 譌・譛ャ隱 と置換文字
b = u.encode('cp932', errors='replace')
print(' '.join('%02X' % x for x in b))      # E6 97 A5 E6 9C AC E8 AA 3F
print(b.decode('utf-8', errors='replace'))  # 日本 と U+FFFD と ? -- 語は復元不能
"

# エラーを出さずに1回で情報が消える例:
python3 -c "print('日本語'.encode('cp1252', errors='replace'))"   # b'???'

境界ごとに文字コードを固定して再発を止める

文字化けは受け渡しの境界で起きます。ファイルがツールからツール、システムから人へ移るところです。自動判定は優秀でも完璧ではないので、境界ごとに文字コードを明示します。

  • ファイル: UTF-8に統一し、BOMを付けるかどうかを1つ決めます。BOMがあると表計算ソフトでCSVをダブルクリックしても正しく開ける一方、パーサーによっては1列目の見出しにが混ざります。方針は翻訳者に渡すローカライズキットに書いておきます。
  • データベース: 接続と列の文字コードのずれは、二重エンコードの典型的な原因です。MySQLの文字セットの章によれば、utf8mb3は4バイト文字を格納できない3バイトの部分集合で、utf8が指す先はバージョンで変わります。絵文字や一部の漢字が入る場所はutf8mb4と明示します(ゲームテキストの絵文字とサロゲートペア)。
  • エディタ: 文字コードの自動判定は可能なら切り、プロジェクト単位で明示します。
  • ゲームエンジン: インポーターが何を前提にしているかを確認します。化けがゲーム実行中にだけ出てソースファイルでは出ない場合、文字コードは正常で問題は下流です(Windowsシステムロケールによる文字化け)。
  • 回帰テスト: こんにちは・café・絵文字を含む行をテストデータに入れ、取り込みと書き出しのたびに突き合わせます。壊れる境界があれば、出荷後ではなく壊れた当日にその行が落ちます。目視なら文字化けパターン早見表をテストの隣に置きます。

関連記事