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 の考え方をベースに、
といった独自対応を行っています。
※テキストファイルの内容によっては、文字コード的に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; } } }
使い方:
- 文字コード判別したいテキストファイルの内容を、byte配列に読み込む
- 読み込んだファイルの内容にBOMが付いていないか確認し、BOMなしであればdetectCharCode()を呼び出す。引数にはbyte配列とファイル長を指定する。(byte配列長>ファイル長でもOKです)
- 判定結果に応じた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