G21 【外伝】はいはい文字化け文字化け UTF-8 Shift_JIS BOMを疑う順番
連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
「はいはい文字化けね」で終わらせると、だいたい長引くことがあります。
文字コードは「原因を当てる」より、疑う順番を揃えた方が早いです。
現場で止まりやすいのは、次のような場面です。
- 同じCSV/TSVのはずなのに、日本語だけ崩れる(文字化けする)
- 先頭行だけ列がズレる/ヘッダ一致だけ失敗する
- 「UTF-8で渡した」と言われたのに、こちらでは再現しない
- バイトで切ったら文字が欠ける/例外になる
半角は1バイト、全角は2バイト以上。そこまでは分かっていても、切り分けに必要なのはその先です。
どう読むか(UTF-8 / Shift_JIS) と 先頭に何が混ざるか(BOM) を、順番で整理します。
1. このページで押さえること
- UTF-8 / Shift_JIS の違いを「読み方の違い」として説明できる
- BOM(先頭の印)が混ざると何が起きるかを説明できる
- 「文字数」と「バイト数」を混同せずに扱える
- .NET 8 を基本に、.NET Framework 4.8でも考え方は同じ
2. 結論(疑う順番)
2-1. まずはこの順番
文字化けや先頭ズレに気づいた瞬間、いきなりUTF-8/Shift_JISを当てに行くと外しやすいです。
最初にやるべきは「文字コードを当てる」ではなく、入力の前提を揃えることになります。
なぜなら、文字コードは「推理」ではなく 運用の前提 で決まることが多いからです。
同じUTF-8でも、BOMの有無や、相手側の都合(Excel/古い連携)で読み方が変わることもあります。
まず 1) で確認するのは、この2点です。
- 入力の前提を確認する
- どこから来たテキストか(Excelで手作り/外部システムの連携/別アプリの出力/ログ保存など)
- 受け側で「何として読むか」を決められるか(規約化できるか)
ここが揃うと、次に何を疑うべきかが絞れます。
- Excel由来なら「Shift_JIS」や「UTF-8(BOMあり)」が混ざりやすい
- 外部連携なら、相手の仕様書に「文字コード」「BOM有無」が書かれていることが多い
- 自システム内の生成物なら、こちらの出力側をUTF-8に寄せて統一できることが多い
- ログなら、書き出し側のEncodingが決まっているので、読む側はそれに合わせるだけで済むことが多い
前提が揃ったら、疑う順番は次です。
- 先頭だけおかしいならBOMを疑う
- ヘッダ一致だけ失敗する
- 先頭列だけズレる
- 先頭に見えない文字が混ざる
- 文字化けならUTF-8とShift_JISの読み方を疑う
- 送った側はUTF-8のつもり
- 受け側はShift_JISのつもり
このすれ違いが一番多いです。
- 切り詰め/固定長/上限チェックなら「文字」と「バイト」を分けて考える
-
string.Lengthは「見た目の文字数」ではありません - バイト数は
Encoding.GetByteCount側で決まります
2-2. 一度覚えておくとよい判断フロー
3. BOM(先頭の印)を疑うポイント
BOMは、ファイル先頭に付く「印」です。
UTF-8でも付くことがあり、先頭3バイト EF BB BF が混ざります。
BOMが混ざると、現場ではこう見えます。
- CSV/TSVの先頭ヘッダだけ一致しない
- 先頭列だけキーが見つからない
- 先頭だけ変な文字が混ざって見える
3-1. BOMで先頭ヘッダが一致しなくなる例
Id のつもりが \uFEFFId になり、比較が落ちることがあります。
見た目では分かりません。
// 先頭にBOM由来のU+FEFFが混ざることがあります
static string RemoveLeadingBomChar(string s)
{
if (string.IsNullOrEmpty(s)) return s;
return s[0] == '\uFEFF' ? s.Substring(1) : s;
}
CSVのヘッダ照合で引っかかる場合は、最初のトークンだけでも除去して差分が出るか確認すると早いです。
3-2. バイト列でBOMを確認する(最短)
static bool HasUtf8Bom(byte[] bytes)
{
return bytes.Length >= 3
&& bytes[0] == 0xEF
&& bytes[1] == 0xBB
&& bytes[2] == 0xBF;
}
4. UTF-8 と Shift_JIS(読み方の違い)
文字コードの違いは、ざっくり言うと「同じバイト列をどう読むか」の違いです。
UTF-8で書いたものをShift_JISとして読むと崩れ、逆も同様です。
4-1. 実務で多い組み合わせ
- 自システム内はUTF-8で統一しているつもり
- ただしExcel経由のCSVはShift_JIS(またはUTF-8 BOMあり)が混ざる
- 外部連携は相手の都合でShift_JISが来ることがある
この混在が「端末だけ」「ファイルだけ」に見える原因になりがちです。
4-2. 読み込み側はEncodingを明示する
「既定に任せる」が一番ブレます。
読み込み側で明示し、想定外なら弾く/分岐する設計へ寄せます。
using System.Text;
var textUtf8 = File.ReadAllText(path, Encoding.UTF8);
var textSjis = File.ReadAllText(path, Encoding.GetEncoding("shift_jis"));
4-3. .NET 8でShift_JISを使う時の注意
.NET 8 では、Shift_JISなどのコードページを使うために登録が必要になることがあります。
using System.Text;
// 起動時に一度だけ
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// 以降、shift_jisが取得できます
var sjis = Encoding.GetEncoding("shift_jis");
.NET Framework 4.8は、環境によって最初から使えることが多いです。
5. 文字数とバイト数を混同しない
「半角=1バイト、全角=2バイト以上」の知識は入口として有用です。
ただし実務で詰まるのは、次の混同です。
-
string.Lengthを「バイト数」だと思って切る - 固定長ファイルの仕様(バイト単位)を「文字数」で処理してしまう
- エンコーディングが変わるのに上限チェックが同じだと思ってしまう
5-1. バイト数はEncodingで決まる
using System.Text;
var s = "Aあ😀";
Console.WriteLine($"Length(char数): {s.Length}");
Console.WriteLine($"UTF-8 bytes: {Encoding.UTF8.GetByteCount(s)}");
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var sjis = Encoding.GetEncoding("shift_jis");
Console.WriteLine($"Shift_JIS bytes: {sjis.GetByteCount(s)}");
Length は「UTF-16の要素数」に近く、見た目の文字数と一致しないことがあります。
バイト数は GetByteCount 側で確認するとズレません。
6. 実務レシピ(迷いを減らす)
6-1. まず規約にできるかを確認する
- 入力はUTF-8に統一できるか
- BOMは付けるか付けないか
- 改行はCRLF/LFどちらに寄せるか
- Excel要件があるなら、Excelで想定する形式(UTF-8 BOMあり/Shift_JIS)を明確にする
規約にできるなら、推測より規約が強いです。
6-2. 規約にできない場合は「受け口で分岐」を用意する
- 受け口でBOMの有無を確認する
- UTF-8/Shift_JISのどちらとして扱うかを選べるようにする
- 判定できない場合は「読めない」扱いにしてログへ残す
「なんとなく読めた」状態で先へ進むと、後で原因が追えなくなります。
6-3. ログに残す観点(切り分けを速くする)
最低限、次が残っていると切り分けが速くなります。
- ファイルサイズ
- BOMの有無
- 読み込みに使ったEncoding名(UTF-8 / Shift_JISなど)
- 先頭行(必要なら先頭数十バイトのHex)
7. チェックリスト(レビューで止める)
- File I/OでEncodingを明示している
- BOM方針(付ける/付けない)が決まっている、混在させない
- 先頭ヘッダ不一致の時にBOM混入を疑える作りになっている
-
string.LengthとEncoding.GetByteCountを混同していない - .NET 8でShift_JISを扱う場合に
Encoding.RegisterProvider(...)が入っている - Excel要件がある場合に「Excelで読む前提の形式」が明記されている