C# Tips | ファイル・ディレクトリ操作:ファイルエンコーディング判定

C sharp C#
スポンサーリンク

はじめに 「エンコーディング判定」が分からないとハマる世界

業務でテキストファイル(CSV、ログ、設定ファイルなど)を扱うとき、
ほぼ必ず出てくるのが「このファイル、何のエンコーディングで書かれているの?」問題です。

UTF-8 だと思って読んだら文字化けした。
Shift_JIS だと思って処理したら、一部の記号だけおかしい。
外部システムから来るファイルのエンコーディングがバラバラでつらい。

こういうときに欲しくなるのが「ファイルエンコーディング判定」ユーティリティです。
ただし、ここがとても大事なポイントですが、「エンコーディングは完全には自動判定できない」という前提からスタートする必要があります。

そのうえで、「できる範囲で賢く推測する」「BOM を見て確実に分かるものだけでも判定する」といった現実的なアプローチを取ります。

ここでは、初心者向けに
「そもそもエンコーディングとは何か」
「C# で何がどこまで判定できるのか」
「実務で使える“現実解”ユーティリティ」
を、例題付きで丁寧に解説していきます。


エンコーディング判定の前提 「完璧には分からない」を受け入れる

なぜ完全自動判定は無理なのか

テキストファイルは、ただの「バイト列」です。
そのバイト列を「どう解釈するか」がエンコーディングです。

同じバイト列でも、
UTF-8 として読むのか、Shift_JIS として読むのかで、
結果の文字列が変わることがあります。

そして、多くのテキストファイルには「これは UTF-8 です」といった情報が埋め込まれていません。
つまり、「見た目」だけでは判断できないケースが必ず存在します。

例えば、英数字だけのファイルは、UTF-8 でも Shift_JIS でも EUC-JP でも、
ほぼ同じバイト列になります。
この場合、「どのエンコーディングか」をバイト列だけから判定するのは不可能です。

だからこそ、実務では次のような割り切りをします。

BOM(バイトオーダーマーク)が付いているなら、それを信じる。
BOM がない場合は、「想定される候補の中から、ある程度のルールで推測する」。
それでも分からない場合は、「既定値(例:Shift_JIS)として扱う」か、「ユーザーに選んでもらう」。

この「完璧ではなく、現実的なラインでやる」という感覚を持っておくと、設計がぶれません。


BOM を使ったエンコーディング判定 「まずはここから」

BOM とは何か

BOM(Byte Order Mark)は、テキストファイルの先頭に付けられる「このファイルはこのエンコーディングですよ」という目印のようなものです。

例えば、代表的なものは次の通りです。

UTF-8 BOM
先頭 3 バイトが EF BB BF

UTF-16 LE BOM
先頭 2 バイトが FF FE

UTF-16 BE BOM
先頭 2 バイトが FE FF

BOM が付いているファイルなら、「このエンコーディングだ」とかなり確信を持って言えます。

BOM を見て判定するユーティリティ

まずは、「BOM がある場合だけ判定する」シンプルなユーティリティを書いてみます。

using System;
using System.IO;
using System.Text;

public static class EncodingDetector
{
    public static Encoding? DetectByBom(string path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        byte[] bom = new byte[4];

        using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            int read = stream.Read(bom, 0, bom.Length);

            if (read >= 3 &&
                bom[0] == 0xEF &&
                bom[1] == 0xBB &&
                bom[2] == 0xBF)
            {
                return Encoding.UTF8;
            }

            if (read >= 2 &&
                bom[0] == 0xFF &&
                bom[1] == 0xFE)
            {
                return Encoding.Unicode;
            }

            if (read >= 2 &&
                bom[0] == 0xFE &&
                bom[1] == 0xFF)
            {
                return Encoding.BigEndianUnicode;
            }

            return null;
        }
    }
}
C#

使い方の例です。

string path = @"C:\data\sample.txt";

Encoding? enc = EncodingDetector.DetectByBom(path);

if (enc is null)
{
    Console.WriteLine("BOM からはエンコーディングを判定できませんでした。");
}
else
{
    Console.WriteLine($"BOM から判定されたエンコーディング: {enc.WebName}");
}
C#

ここでの重要ポイントは、「BOM がない場合は null を返す」という設計にしていることです。
「分からないときは分からないと言う」ことが、エンコーディング判定ではとても大事です。


BOM がない場合の“現実的な”判定戦略

「候補を決めて、その中から推測する」という考え方

日本の業務システムだと、よく出てくるエンコーディングはだいたい決まっています。

UTF-8(BOM あり/なし)
Shift_JIS(CP932)

この 2 つを候補にして、「どちらとして読んだときに“それっぽい”か」を判定する、という戦略が現実的です。

例えば、次のような方針が考えられます。

まず BOM を見る。
BOM がなければ、「UTF-8 として読んでみて、変なバイト列がないかチェックする」。
UTF-8 として成立しないバイト列があれば、「Shift_JIS とみなす」。
どちらとしても成立しそうなら、「既定値(例:UTF-8)を採用する」。

ここで「UTF-8 として成立するかどうか」をチェックするには、
Encoding.UTF8 でデコードしてみて、例外や置換が発生するかどうかを見る方法があります。

UTF-8 と Shift_JIS をざっくり判定する例

「厳密ではないけれど、実務ではそこそこ使える」レベルのサンプルを書いてみます。

using System;
using System.IO;
using System.Text;

public static class EncodingDetector
{
    public static Encoding DetectEncodingWithFallback(string path)
    {
        var bomEncoding = DetectByBom(path);
        if (bomEncoding != null)
        {
            return bomEncoding;
        }

        byte[] bytes = File.ReadAllBytes(path);

        if (LooksLikeUtf8(bytes))
        {
            return new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
        }

        return Encoding.GetEncoding(932);
    }

    private static bool LooksLikeUtf8(byte[] bytes)
    {
        try
        {
            var utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
            utf8.GetString(bytes);
            return true;
        }
        catch (DecoderFallbackException)
        {
            return false;
        }
    }

    public static Encoding? DetectByBom(string path)
    {
        if (!File.Exists(path))
        {
            throw new FileNotFoundException("ファイルが存在しません。", path);
        }

        byte[] bom = new byte[4];

        using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            int read = stream.Read(bom, 0, bom.Length);

            if (read >= 3 &&
                bom[0] == 0xEF &&
                bom[1] == 0xBB &&
                bom[2] == 0xBF)
            {
                return Encoding.UTF8;
            }

            if (read >= 2 &&
                bom[0] == 0xFF &&
                bom[1] == 0xFE)
            {
                return Encoding.Unicode;
            }

            if (read >= 2 &&
                bom[0] == 0xFE &&
                bom[1] == 0xFF)
            {
                return Encoding.BigEndianUnicode;
            }

            return null;
        }
    }
}
C#

使い方の例です。

string path = @"C:\data\import.csv";

Encoding enc = EncodingDetector.DetectEncodingWithFallback(path);

Console.WriteLine($"推定エンコーディング: {enc.WebName}");
C#

ここでの重要ポイントは二つです。

一つ目は、「UTF-8 としてデコードしてみて、ダメなら Shift_JIS とみなす」という割り切りです。
日本の業務システムでは、「UTF-8 か Shift_JIS のどちらか」という前提が多いので、この戦略はかなり現実的です。

二つ目は、「UTF-8 のインスタンスを作るときに throwOnInvalidBytes: true を指定している」ことです。
これにより、「UTF-8 として不正なバイト列」があれば DecoderFallbackException が投げられます。
それをキャッチして「UTF-8 ではなさそう」と判断しています。

もちろん、これは万能ではありません。
英数字だけのファイルなど、「どちらとしても成立する」ケースでは、
最終的には「既定値(ここでは UTF-8)」に倒すしかありません。


実務ユーティリティとしての設計

「判定結果」と「判定方法」をセットで返す

業務で使うなら、「どうやってそのエンコーディングだと判断したか」も一緒に持っておくと便利です。

例えば、次のような結果クラスを用意します。

public enum EncodingDetectionSource
{
    Bom,
    Utf8Heuristic,
    DefaultFallback
}

public sealed class EncodingDetectionResult
{
    public Encoding Encoding { get; }
    public EncodingDetectionSource Source { get; }

    public EncodingDetectionResult(Encoding encoding, EncodingDetectionSource source)
    {
        Encoding = encoding;
        Source = source;
    }
}
C#

そして、ユーティリティをこう書き換えます。

public static class EncodingDetector
{
    public static EncodingDetectionResult DetectEncoding(string path)
    {
        var bomEncoding = DetectByBom(path);
        if (bomEncoding != null)
        {
            return new EncodingDetectionResult(bomEncoding, EncodingDetectionSource.Bom);
        }

        byte[] bytes = File.ReadAllBytes(path);

        if (LooksLikeUtf8(bytes))
        {
            var utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
            return new EncodingDetectionResult(utf8NoBom, EncodingDetectionSource.Utf8Heuristic);
        }

        var sjis = Encoding.GetEncoding(932);
        return new EncodingDetectionResult(sjis, EncodingDetectionSource.DefaultFallback);
    }

    // DetectByBom と LooksLikeUtf8 は先ほどと同じ
}
C#

使い方の例です。

string path = @"C:\data\import.csv";

var result = EncodingDetector.DetectEncoding(path);

Console.WriteLine($"推定エンコーディング: {result.Encoding.WebName}");
Console.WriteLine($"判定方法: {result.Source}");
C#

これにより、「BOM から確定したのか」「UTF-8 判定ロジックでそうなったのか」「最後の苦し紛れの既定値なのか」が分かります。
ログに残しておけば、「なぜこのエンコーディングで読んだのか」を後から追いやすくなります。


例題:CSV インポート前にエンコーディングを判定する

「とりあえず読んでみて、ダメならエラーにする」より一歩進める

よくあるシナリオとして、「ユーザーがアップロードした CSV をインポートする」ケースを考えてみます。

単純に「UTF-8 決め打ちで読んで、例外が出たらエラー」とするより、
事前にエンコーディングを推定してから読むほうが、ユーザー体験もログも良くなります。

string csvPath = @"C:\upload\sales.csv";

var detection = EncodingDetector.DetectEncoding(csvPath);

Console.WriteLine($"推定エンコーディング: {detection.Encoding.WebName} ({detection.Source})");

string[] lines = File.ReadAllLines(csvPath, detection.Encoding);

Console.WriteLine($"行数: {lines.Length}");
C#

ここで、「判定方法」が DefaultFallback だった場合だけ、
「エンコーディングが曖昧なので、文字化けの可能性があります」といった警告を出す、
といった工夫もできます。


まとめ 実務で使える「ファイルエンコーディング判定」ユーティリティの考え方

エンコーディング判定は、「完璧に当てる」ことを目指すと必ず破綻します。
大事なのは、「どこまで確実に分かるか」「どこからは推測か」「どこからは諦めるか」をはっきりさせることです。

押さえておきたいポイントは次の通りです。

BOM があるファイルはラッキーゾーン。BOM を見ればかなり確実に判定できる。
BOM がない場合は、「候補を絞って、その中から推測する」戦略を取る(日本の業務なら UTF-8 と Shift_JIS が多い)。
UTF-8 かどうかは、「UTF-8 としてデコードしてみて、不正バイトがあるかどうか」でざっくり判定できる。
判定結果だけでなく、「どうやってそう判断したか(BOM/推測/既定値)」も一緒に持っておくと、ログやトラブルシュートで役立つ。
「分からないときは分からないと言う」設計にしておくと、無理な自動判定でハマるリスクを減らせる。

ここまで押さえておけば、「なんとなく UTF-8 で読んでみる」から卒業して、
「業務で通用する、現実的なエンコーディング判定ユーティリティ」を自信を持って設計できるようになります。

タイトルとURLをコピーしました