HNXgrepのC#による文字コード判定

※2014.08.24追記
 この記事の文字コード判別ソースコードは、2012年時点の古いバージョンのものです。
 最新バージョンの文字コード判別は、「C#で高精度なテキストファイル文字コード自動判別(2014年版) - hnx8 開発室」の記事を参照ください。

C#(.net Framework)には、テキストファイルの文字コードエンコーディング)を自動判別して読み込むような機能、JavaのJISAutoDetectに相当する機能は用意されていません。
なので、読み込むテキストファイルの文字コードは自前で判定する、もしくはそういう機能をもつ外部dllを利用する必要があります。

拙作ソフトHNXgrep http://www.vector.co.jp/soft/winnt/util/se494966.html では、独自実装のソースコード文字コードの判定を行っています。
ASCII,JIS、EUC,ShiftJIS,UTF8、および(半角英数文字のみで書かれている)UTF16Nの判定が出来ます。
基本的には、DOBON.NET様のサイトで公開されているJcode.pmのC#移植版 http://dobon.net/vb/dotnet/string/detectcode.html の考え方をベースに、

  • UTF16(BOMなし半角英数のみ)ファイルの検出機能を追加
  • EUC/SJISの判別を強化するため、半角カナ文字の連続/全角文字の連続についてはポイント評価を高めに補正(※)

といった独自対応を行っています。
※テキストファイルの内容によっては、文字コード的にShiftJISとしてもEUCとしても妥当であり、またUTF8としても妥当であるような場合があり得ます。そのため、どの文字コードでデコードするのがより適切なのかを決定する基準として、全角半角文字が連続していることを重要視してみました。(ただしポイント配点はざっくり適当に決めています)

2012.02.25時点の文字コード判別ソースコードを以下に公開します。
2014.05.13:ESC(JIS)判定の2バイト目以降把握のバグを修正しました。黒い猫さん、ありがとうございました!
バグ・ロジック上の問題点などを見つけた方はぜひぜひご指摘ください。

class CharCodeDetector
{
    //文字コードの種類
    enum CharCode
    {
        ASCII,
        BINARY,
        EUC,
        JIS,
        SJIS,
        UTF16BE,
        UTF16LE,
        UTF8N
    }

    /// <summary>
    /// 読み込んでいるbyte配列内容のエンコーディングを自前で判定する
    /// </summary>
    /// <param name="data">ファイルから読み込んだバイトデータ</param>
    /// <param name="datasize">バイトデータのサイズ</param>
    /// <returns>エンコーディングの種類</returns>
    public CharCode detectCharCode(byte[] data, int datasize)
    {
        //バイトデータ(読み取り結果)
        byte b1 = (datasize > 0) ? data[0] : (byte)0;
        byte b2 = (datasize > 1) ? data[1] : (byte)0;
        byte b3 = (datasize > 2) ? data[2] : (byte)0;
        byte b4 = (datasize > 3) ? data[3] : (byte)0;

        //UTF16Nの判定(ただし半角英数文字の場合のみ検出可能)
        if (b1 == 0x00 && (datasize % 2 == 0))
        {
            for (int i = 0; i < datasize; i = i + 2)
            {
                if (data[i] != 0x00 || data[i + 1] < 0x06 || data[i + 1] >= 0x7f)
                {   //半角OnlyのUTF16でもなさそうなのでバイナリ
                    return CharCode.BINARY;
                }
            }
            return CharCode.UTF16BE;
        }
        if (b2 == 0x00 && (datasize % 2 == 0))
        {
            for (int i = 0; i < datasize; i = i + 2)
            {
                if (data[i] < 0x06 || data[i] >= 0x7f || data[i + 1] != 0x00)
                {   //半角OnlyのUTF16でもなさそうなのでバイナリ
                    return CharCode.BINARY;
                }
            }
            return CharCode.UTF16LE;
        }

        //全バイト内容を走査・まずAscii,JIS判定
        int pos = 0;
        int jisCount = 0;
        while (pos < datasize)
        {
            b1 = data[pos];
            if (b1 < 0x03 || b1 >= 0x7f)
            {   //非ascii(UTF,SJis等)発見:次のループへ
                break;
            }
            else if (b1 == 0x1b)
            {   //ESC(JIS)判定
                //2バイト目以降の値を把握
                b2 = ((pos + 1 < datasize) ? data[pos + 1] : (byte)0);
                b3 = ((pos + 2 < datasize) ? data[pos + 2] : (byte)0);
                b4 = ((pos + 3 < datasize) ? data[pos + 3] : (byte)0);
                //B2の値をもとに判定
                if (b2 == 0x24)
                {   //ESC$
                    if (b3 == 0x40 || b3 == 0x42)
                    {   //ESC $@,$B : JISエスケープ
                        jisCount++;
                        pos = pos + 2;
                    }
                    else if (b3 == 0x28 && (b4 == 0x44 || b4 == 0x4F || b4 == 0x51 || b4 == 0x50))
                    {   //ESC$(D, ESC$(O, ESC$(Q, ESC$(P : JISエスケープ
                        jisCount++;
                        pos = pos + 3;
                    }
                }
                else if (b2 == 0x26)
                {   //ESC& : JISエスケープ
                    if (b3 == 0x40)
                    {   //ESC &@ : JISエスケープ
                        jisCount++;
                        pos = pos + 2;
                    }
                }
                else if (b2 == 0x28)
                {   //ESC((28)
                    if (b3 == 0x4A || b3 == 0x49 || b3 == 0x42)
                    {   //ESC(J, ESC(I, ESC(B : JISエスケープ
                        jisCount++;
                        pos = pos + 2;
                    }
                }
            }
            pos++;
        }
        //Asciiのみならここで文字コード決定
        if (pos == datasize)
        {
            if (jisCount > 0)
            {   //JIS出現
                return CharCode.JIS;
            }
            else
            {   //JIS未出現。Ascii
                return CharCode.ASCII;
            }
        }

        bool prevIsKanji = false; //文字コード判定強化、同種文字のときにポイント加算-HNXgrep
        int notAsciiPos = pos;
        int utfCount = 0;
        //UTF妥当性チェック(バイナリ判定を行いながら実施)
        while (pos < datasize)
        {
            b1 = data[pos];
            pos++;

            if (b1 < 0x03 || b1 == 0x7f || b1 == 0xff)
            {   //バイナリ文字:直接脱出
                return CharCode.BINARY;
            }
            if (b1 < 0x80 || utfCount < 0)
            {   //半角文字・非UTF確定時は、後続処理は行わない
                continue; // 半角文字は特にチェックしない
            }

            //2バイト目を把握、コードチェック
            b2 = ((pos < datasize) ? data[pos] : (byte)0x00);
            if (b1 < 0xC2 || b1 >= 0xf5)
            {   //1バイト目がC0,C1,F5以降、または2バイト目にしか現れないはずのコードが出現、NG
                utfCount = -1;
            }
            else if (b1 < 0xe0)
            {   //2バイト文字:コードチェック
                if (b2 >= 0x80 && b2 <= 0xbf)
                {   //2バイト目に現れるべきコードが出現、OK(半角文字として扱う)
                    if (prevIsKanji == false) { utfCount += 2; } else { utfCount += 1; prevIsKanji = false; }
                    pos++;
                }
                else
                {   //2バイト目に現れるべきコードが未出現、NG
                    utfCount = -1;
                }
            }
            else if (b1 < 0xf0)
            {   //3バイト文字:3バイト目を把握
                b3 = ((pos + 1 < datasize) ? data[pos + 1] : (byte)0x00);
                if (b2 >= 0x80 && b2 <= 0xbf && b3 >= 0x80 && b3 <= 0xbf)
                {   //2/3バイト目に現れるべきコードが出現、OK(全角文字扱い)
                    if (prevIsKanji == true) { utfCount += 4; } else { utfCount += 3; prevIsKanji = true; }
                    pos += 2;
                }
                else
                {   //2/3バイト目に現れるべきコードが未出現、NG
                    utfCount = -1;
                }
            }
            else
            {   //4バイト文字:3,4バイト目を把握
                b3 = ((pos + 1 < datasize) ? data[pos + 1] : (byte)0x00);
                b4 = ((pos + 2 < datasize) ? data[pos + 2] : (byte)0x00);
                if (b2 >= 0x80 && b2 <= 0xbf && b3 >= 0x80 && b3 <= 0xbf && b4 >= 0x80 && b4 <= 0xbf)
                {   //2/3/4バイト目に現れるべきコードが出現、OK(全角文字扱い)
                    if (prevIsKanji == true) { utfCount += 6; } else { utfCount += 4; prevIsKanji = true; }
                    pos += 3;
                }
                else
                {   //2/3/4バイト目に現れるべきコードが未出現、NG
                    utfCount = -1;
                }
            }
        }

        //SJIS妥当性チェック
        pos = notAsciiPos;
        int sjisCount = 0;
        while (sjisCount >= 0 && pos < datasize)
        {
            b1 = data[pos];
            pos++;
            if (b1 < 0x80) { continue; }// 半角文字は特にチェックしない
            else if (b1 == 0x80 || b1 == 0xA0 || b1 >= 0xFD)
            {   //SJISコード外、可能性を破棄
                sjisCount = -1;
            }
            else if ((b1 > 0x80 && b1 < 0xA0) || b1 > 0xDF)
            {   //全角文字チェックのため、2バイト目の値を把握
                b2 = ((pos < datasize) ? data[pos] : (byte)0x00);
                //全角文字範囲外じゃないかチェック
                if (b2 < 0x40 || b2 == 0x7f || b2 > 0xFC)
                {   //可能性を除外
                    sjisCount = -1;
                }
                else
                {   //全角文字数を加算,ポジションを進めておく
                    if (prevIsKanji == true) { sjisCount += 2; } else { sjisCount += 1; prevIsKanji = true; }
                    pos++;
                }
            }
            else if (prevIsKanji == false)
            {
                //半角文字数の加算(半角カナの連続はボーナス点を高めに)
                sjisCount += 1;
            }
            else
            {
                prevIsKanji = false;
            }
        }
        //EUC妥当性チェック
        pos = notAsciiPos;
        int eucCount = 0;
        while (eucCount >= 0 && pos < datasize)
        {
            b1 = data[pos];
            pos++;
            if (b1 < 0x80) { continue; } // 半角文字は特にチェックしない
            //2バイト目を把握、コードチェック
            b2 = ((pos < datasize) ? data[pos] : (byte)0);
            if (b1 == 0x8e)
            {   //1バイト目=かな文字指定。2バイトの半角カナ文字チェック
                if (b2 < 0xA1 || b2 > 0xdf)
                {   //可能性破棄
                    eucCount = -1;
                }
                else
                {   //検出OK,EUC文字数を加算(半角文字)
                    if (prevIsKanji == false) { eucCount += 2; } else { eucCount += 1; prevIsKanji = false; }
                    pos++;
                }
            }
            else if (b1 == 0x8f)
            {   //1バイト目の値=3バイト文字を指定
                if (b2 < 0xa1 || (pos + 1 < datasize && data[pos + 1] < 0xa1))
                {   //2バイト目・3バイト目で可能性破棄
                    eucCount = -1;
                }
                else
                {   //検出OK,EUC文字数を加算(全角文字)
                    if (prevIsKanji == true) { eucCount += 3; } else { eucCount += 1; prevIsKanji = true; }
                    pos += 2;
                }
            }
            else if (b1 < 0xa1 || b2 < 0xa1)
            {   //2バイト文字のはずだったがどちらかのバイトがNG
                eucCount = -1;
            }
            else
            {   //2バイト文字OK(全角)
                if (prevIsKanji == true) { eucCount += 2; } else { eucCount += 1; prevIsKanji = true; }
                pos++;
            }
        }

        //文字コード決定
        if (eucCount > sjisCount && eucCount > utfCount)
        {
            return CharCode.EUC;
        }
        else if (utfCount > sjisCount)
        {
            return CharCode.UTF8N;
        }
        else if (sjisCount > -1)
        {
            return CharCode.SJIS;
        }
        else
        {
            return CharCode.BINARY;
        }
    }
}

使い方:

  1. 文字コード判別したいテキストファイルの内容を、byte配列に読み込む
  2. 読み込んだファイルの内容にBOMが付いていないか確認し、BOMなしであればdetectCharCode()を呼び出す。引数にはbyte配列とファイル長を指定する。(byte配列長>ファイル長でもOKです)
  3. 判定結果に応じたEncodingクラスのオブジェクトを用意し、encoding.getString(data, 変換開始位置, 変換するバイト数)でbyte配列内容をString文字列に変換する

BOM(Byte Order Mark)つきのテキストファイルは、先頭数バイトの内容から文字コードが決まります。(getStringで文字列へと変換する際には、BOM部分を変換対象に含めないよう注意してください)
BOM判定用のコード(抜粋)も以下に公開しておきます。enumは適宜追加してください。

/// <summary>
/// Bom・ヘッダから決定できる文字コードを判定。
/// </summary>
/// <returns>エンコーディングの種類</returns>
public CharCode detectCharCodeWithBomHeader(byte[] data, int datasize)
{
    //バイトデータ(読み取り結果)
    byte b1 = (datasize > 0) ? data[0] : (byte)0;
    byte b2 = (datasize > 1) ? data[1] : (byte)0;
    byte b3 = (datasize > 2) ? data[2] : (byte)0;
    byte b4 = (datasize > 3) ? data[3] : (byte)1;

    //BOMから判別できる文字コード判定
    if (b1 == 0xFF && b2 == 0xFE && b3 == 0x00 && b4 == 0x00)
    {   //BOMよりUTF32(littleEndian)
        return CharCode.UTF32;
    }
    if (b1 == 0x00 && b2 == 0x00 && b3 == 0xFE && b4 == 0xFF)
    {   //BOMよりUTF32(bigEndian)
        return CharCode.UTF32B;
    }
    if (b1 == 0xff && b2 == 0xfe)
    {   //BOMよりUnicode(Windows標準のUTF-16のlittleEndian)
        return CharCode.UTF16;
    }
    if (b1 == 0xfe && b2 == 0xff)
    {   //BOMよりUnicode(UTF-16のBigEndien)
        return CharCode.UTF16B;
    }
    if (b1 == 0xef && b2 == 0xbb && b3 == 0xbf)
    {   //BOMよりUTF-8
        return CharCode.UTF8;
    }
    //BOMなし
    return Charcode.Unknown;
}

以上、あまり綺麗なソースコードではありませんが、ShiftJIS判定のバグが見つかったこと(Nabe様、報告ありがとうございました)もあり公開してみることにしました。
C# 文字コード判定」といったキーワードでこのblogにたどり着いた方もいらっしゃるようなので、参考になれば幸いです。

参考にしたサイト:
DOBON.NET 文字コードを判別する
http://dobon.net/vb/dotnet/string/detectcode.html
雅階凡の C# プログラミング C#2008 文字コードの判定
http://www.geocities.jp/gakaibon/tips/csharp2008/charset-check.html