連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index
先頭だけ変なら BOM、全部崩れるなら文字コード違い、桁数だけ外れるなら byte 数を見る。
- CSV の 1 列目だけ合わない
- 日本語全体が
���や別の文字になる - 文字数は合っているのに上限判定だけ外れる
先に分ける表
| 見え方 | まず見る場所 | 典型例 | 最初の確認 |
|---|---|---|---|
| 先頭だけ変 | BOM |
Id だけ見つからない |
先頭 3 byte を見る |
| 全部崩れる | 文字コード違い | 日本語全体が別の文字になる | 同じファイルを UTF-8 と Shift_JIS の両方で読む |
| 桁数だけ外れる | byte 数 | 固定長、切り詰め、上限判定だけずれる |
GetByteCount を見る |
1 列目だけ合わない時は BOM を見る
見た目は Id でも、先頭に \uFEFF が付いていることがある。
CSV のヘッダ比較、辞書キー検索、列名一致で最初だけ外れる時は、ここから見る。
まずは最小の例。
using System;
var actual = "\uFEFFId";
var expected = "Id";
Console.WriteLine(actual == expected);
Console.WriteLine(actual.TrimStart('\uFEFF') == expected);
Console.WriteLine($"[{actual}]");
Console.WriteLine(actual.Length);
出力はこうなる。
False
True
[Id]
3
見た目は Id でも、比較結果は変わる。
先頭だけ外れる時は、文字コード全体より先に BOM を見る方が早い。
ファイルで確認するなら、先頭 3 byte をそのまま出す。
using System;
using System.IO;
using System.Linq;
var path = args[0];
var bytes = File.ReadAllBytes(path);
Console.WriteLine(string.Join(" ", bytes.Take(8).Select(x => x.ToString("X2"))));
var hasUtf8Bom =
bytes.Length >= 3 &&
bytes[0] == 0xEF &&
bytes[1] == 0xBB &&
bytes[2] == 0xBF;
Console.WriteLine($"HasUtf8Bom: {hasUtf8Bom}");
EF BB BF 49 64 2C 4E 61
HasUtf8Bom: True
EF BB BF なら UTF-8 の BOM あり。
自アプリ出力なら BOM あり / なしを決める
ここが曖昧だと、同じ話が戻りやすい。
using System.IO;
using System.Text;
var utf8WithBom = new UTF8Encoding(true);
var utf8WithoutBom = new UTF8Encoding(false);
File.WriteAllText("with-bom.csv", "Id,Name", utf8WithBom);
File.WriteAllText("without-bom.csv", "Id,Name", utf8WithoutBom);
CSV の 1 列目だけ外れる話が何度も出るなら、BOM あり / なしまで仕様へ書いておく方が早い。
日本語全体が崩れる時は文字コード違いを見る
ここは BOM ではなく、同じ byte 列を別の文字コードとして読んでいる 状態が多い。
まずは最小の例。
using System;
using System.Text;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var text = "商品名";
var utf8Bytes = Encoding.UTF8.GetBytes(text);
var broken = Encoding.GetEncoding("shift_jis").GetString(utf8Bytes);
Console.WriteLine(text);
Console.WriteLine(broken);
2 行目は崩れる。
UTF-8 で作った byte 列を Shift_JIS として読んでいるため。
ファイルでも見方は同じ。
既定値のまま読むより、UTF-8 と Shift_JIS の両方で読んだ方が差を追いやすい。
using System;
using System.IO;
using System.Text;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var path = args[0];
var utf8Text = File.ReadAllText(path, Encoding.UTF8);
var shiftJis = Encoding.GetEncoding("shift_jis");
var sjisText = File.ReadAllText(path, shiftJis);
Console.WriteLine("----- UTF-8 -----");
Console.WriteLine(utf8Text.Replace("\r\n", "\n").Split('\n')[0]);
Console.WriteLine("----- Shift_JIS -----");
Console.WriteLine(sjisText.Replace("\r\n", "\n").Split('\n')[0]);
既定値のまま読まない
差が出るのはこの 2 行。
using System.IO;
using System.Text;
// NG
var ngText = File.ReadAllText(path);
// OK
var okText = File.ReadAllText(path, Encoding.UTF8);
ReadAllText(path) だけだと、実行環境ごとの差を追いにくい。
.NET 8 で Shift_JIS を使う時の 1 行
.NET 8 で Shift_JIS を使う時は、先にこれが要る。
using System.Text;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
これがないと Encoding.GetEncoding("shift_jis") が取れない環境がある。
Excel 保存の CSV で見る場所
Excel 経由の CSV は、同じ「CSV 保存」でも中身が変わることがある。
見るのは次の 3 つだけ。
| 確認項目 | 起こりやすい見え方 |
|---|---|
| UTF-8 | UTF-8 前提の処理ならそのまま読める |
| UTF-8 BOM 付き | 1 列目だけ外れることがある |
| Shift_JIS | 日本語全体が崩れることがある |
「Excel で保存した」だけでは足りない。
UTF-8 か UTF-8 BOM 付き か Shift_JIS かまで見る。
桁数だけ外れる時は byte 数を見る
ここは文字化けではなく、長さの見方が違う 状態。
固定長ファイル、外部連携の上限、DB の長さ判定で出やすい。
using System;
using System.Text;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var text = "Aあ";
var utf8 = Encoding.UTF8;
var shiftJis = Encoding.GetEncoding("shift_jis");
Console.WriteLine($"Text: {text}");
Console.WriteLine($"Length: {text.Length}");
Console.WriteLine($"UTF-8 ByteCount: {utf8.GetByteCount(text)}");
Console.WriteLine($"Shift_JIS ByteCount: {shiftJis.GetByteCount(text)}");
出力はこうなる。
Text: Aあ
Length: 2
UTF-8 ByteCount: 4
Shift_JIS ByteCount: 3
Length は 2 でも、byte 数は同じではない。
上限が byte 単位なら、Length ではなく GetByteCount を見る。
差が出る形だとこの 2 行。
using System.Text;
// NG
var isOver = text.Length > 10;
// OK
var isOver = Encoding.UTF8.GetByteCount(text) > 10;
見直す場所はこのあたり。
- 固定長レコードの組み立て
- CSV 出力前の上限判定
- DB 登録前の長さ判定
- byte 単位で切る処理
1 本で確認する検査コード
BOM、UTF-8、Shift_JIS、byte 数をまとめて見る時は、これ 1 本で足りる。
using System;
using System.IO;
using System.Linq;
using System.Text;
internal static class Program
{
private static void Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Usage: TextInspect <filePath>");
return;
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var path = args[0];
var bytes = File.ReadAllBytes(path);
Console.WriteLine($"Path: {path}");
Console.WriteLine($"Length: {bytes.Length}");
Console.WriteLine($"FirstBytes: {string.Join(" ", bytes.Take(8).Select(x => x.ToString("X2")))}");
Console.WriteLine($"HasUtf8Bom: {HasUtf8Bom(bytes)}");
Console.WriteLine();
PrintAs("UTF-8", path, Encoding.UTF8);
Console.WriteLine();
PrintAs("Shift_JIS", path, Encoding.GetEncoding("shift_jis"));
}
private static void PrintAs(string name, string path, Encoding encoding)
{
Console.WriteLine($"----- {name} -----");
try
{
var text = File.ReadAllText(path, encoding);
var firstLine = text.Replace("\r\n", "\n").Split('\n').FirstOrDefault() ?? string.Empty;
Console.WriteLine($"Encoding: {encoding.WebName}");
Console.WriteLine($"FirstLine: {firstLine}");
Console.WriteLine($"ByteCountOfFirstLine: {encoding.GetByteCount(firstLine)}");
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name);
Console.WriteLine(ex.Message);
}
}
private static bool HasUtf8Bom(byte[] bytes)
{
return bytes.Length >= 3
&& bytes[0] == 0xEF
&& bytes[1] == 0xBB
&& bytes[2] == 0xBF;
}
}
dotnet run -- sample.csv
ここで見るのは 4 つで十分。
| 出力 | 何を見るか |
|---|---|
FirstBytes |
EF BB BF があるか |
HasUtf8Bom |
BOM ありか |
FirstLine |
先頭だけ変か、全部変か |
ByteCountOfFirstLine |
byte 数で見た長さ |
仕様へ書く 4 項目
毎回「同じ CSV のはずなのに違う」となるなら、出力条件が曖昧なことが多い。
| 項目 | 例 |
|---|---|
| 文字コード | UTF-8 |
| BOM | なし |
| 改行 | CRLF |
| byte 数で判定する列 | 顧客名、住所、備考 |
文字コードだけでは足りない。
BOM、改行、byte 数で見る列まで書いておくと、確認の戻りが減る。
.NET Framework 4.8.1 の補足
主役は .NET 8 だが、見方そのものは 4.8.1 でもほぼ同じ。
先頭だけ変なら BOM、全部崩れるなら文字コード違い、桁数だけ外れるなら byte 数を見る。
文字化けは広く探すより、見え方で先に分けた方が早い。
連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index