C#による、BOMなしテキストファイルの文字コード(Encoding)判定処理のサンプルコード

shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る

☆2021年11月8日更新: 時々 shift-jisをUTF-8と誤判定する不具合を修正しました。

サンプルコードとしてここに掲載する。

Visual Studio 2017 の .NET Framework 4.0 コンソールアプリで作成している。

.NET バージョンに依存するようなコードでもないので他のバージョンでもビルドできると思う。

 

最初にBOMの有無を判定し、BOMが存在する場合はBOMの文字エンコーディング名を表示する。

 

BOMが存在しなければ、文字コードのバイナリ値から、文字エンコーディングを推測する。

 

コマンドラインから、

 

EncodingJudgment <テキストファイル名>

 

と入力すると判定結果を表示する。

表示するのは「文字エンコーディング名」と「コードページ」と「BOMの有無」である。

 

推測方法としては、規格の厳しい文字エンコーディングから順に、文字エンコーディングの規格当てはまらないケースが存在しないか確認し、存在すれば「その文字エンコーディングでは無い」と判断して、次のエンコーディングを検査する。

検査は先頭1000byteのテキストコードだけで行う。

 

全ての文字エンコーディングがあり得なければ、I do not know. と表示する。

 

検査対象となる文字エンコーディングは、

BOM有りなら、

utf-8, utf-16LE, utf-16BE, utf-32LE, utf-32BE

 

BOMなしなら、

us-ascii, iso-2022-jp, utf-8, euc-jp, shift_jis

となる。

 

utf-16LE, utf-16BE, utf-32LE, utf-32BE の場合は、BOMなしでは判別できないので、自動判定の対象にはしていない。

 

最初にお断りしておくが、文字エンコーディングの自動推測は完全にはできない。

特に shift_jis の判定は難しく、このサンプルでも、他の文字エンコーディングの規格照合を先にやって、最後に shift_jis の判定を行っている。

他の文字エンコーディングは規格が厳しく、規格外判定がやりやすいが、 shift_jis はその判定が難しく、他の文字エンコーディング規格に当てはまらない事が判定基準の重要な要素になっている。

よって確実に動作する事は保証できない。

これは他の文字エンコーディング判定プログラムでも同じである。

ご自分でビルドして動作確認してみる事をお勧めする。

 

<2020-10-29追記>ゼロサイズのテキストファイルに対応しました。

 

では全てのソースコードを以下に掲載する。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace EncodingJudgment
{
class Program
{
static void Main(string[] args)
{
if(args.Length < 1)
{
Console.WriteLine("Please enter the file name.");
return;
}

string filename = args[0];

const int BufferSize = 1000;

using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
byte[] readBuffer = new byte[BufferSize];

int readCount = fs.Read(readBuffer, 0, BufferSize);
//ゼロサイズのutf-16LE.BE 対応
readBuffer[2] = 0xFF;
readBuffer[3] = 0xFF;

bool bomExist;

int codePage = EncodingJudgment(readBuffer, BufferSize, out bomExist);

string encodingName;

switch (codePage)
{
case 20127:
encodingName = "us-ascii";
break;
case 50220:
encodingName = "iso-2022-jp";
break;
case 65001:
encodingName = "utf-8";
break;
case 20932:
encodingName = "euc-jp";
break;
case 932:
encodingName = "shift_jis";
break;
case 1200:
encodingName = "utf-16";
break;
case 1201:
encodingName = "unicodeFFFE";
break;
case 12000:
encodingName = "utf-32";
break;
case 12001:
encodingName = "utf-32BE";
break;
default:
encodingName = "I do not know.";
break;
}

Console.WriteLine("Encoding = {0} , Codepage = {1} , BOM = {2}", encodingName, codePage, bomExist);
}

}


public static int EncodingJudgment(byte[] buffer, int sizeOfBuffer, out bool bomExist)
{
int codePage = 0;
bool outOfSpecification;

// Check BOM
if(IsBOM(buffer, out codePage))
{
bomExist = true;
return codePage;
}
else
{
bomExist = false;
}

// if ISO-2022-JP or ASCII
bool isJIS;
outOfSpecification = JISEncodingJudgment(buffer, sizeOfBuffer, out isJIS);

if(outOfSpecification == false)
{
if(isJIS == true)
{
codePage = 50220; //iso-2022-jp : Japanese (JIS)
}
else
{
codePage = 20127; //us-ascii : US-ASCII
}
return codePage;
}

// else if UTF-8
outOfSpecification = Utf8EncodingJudgment(buffer, sizeOfBuffer);

if (outOfSpecification == false)
{
codePage = 65001; //utf-8 : Unicode (UTF-8)
return codePage;
}

// else if EUC-JP
outOfSpecification = EUCJPEncodingJudgment(buffer, sizeOfBuffer);

if (outOfSpecification == false)
{
codePage = 20932; //EUC-JP : Japanese (JIS 0208-1990 and 0212-1990)
return codePage;
}

// else if Shift_JIS
outOfSpecification = SJISEncodingJudgment(buffer, sizeOfBuffer);

if (outOfSpecification == false)
{
codePage = 932; //shift_jis : Japanese (Shift-JIS)
return codePage;
}


// I do not know.


return codePage;
}

public static bool IsBOM(byte[] bomByte, out int codepage)
{
bool result;
byte[] bomUTF8 = { 0xEF, 0xBB, 0xBF };
byte[] bomUTF16Little = { 0xFF, 0xFE };
byte[] bomUTF16Big = { 0xFE, 0xFF };
byte[] bomUTF32Little = { 0xFF, 0xFE, 0x00, 0x00 };
byte[] bomUTF32Big = { 0x00, 0x00, 0xFE, 0xFF };

if (IsMatched(bomByte, bomUTF8))
{
result = true;
codepage = 65001; //utf-8,Unicode (UTF-8)
}

else if (IsMatched(bomByte, bomUTF32Little))
{
result = true;
codepage = 12000; //utf-32,Unicode (UTF-32)
}

else if (IsMatched(bomByte, bomUTF32Big))
{
result = true;
codepage = 12001; //utf-32BE,Unicode (UTF-32 Big-Endian)
}

else if (IsMatched(bomByte, bomUTF16Little))
{
result = true;
codepage = 1200; //utf-16,Unicode
}

else if (IsMatched(bomByte, bomUTF16Big))
{
result = true;
codepage = 1201; //utf-16BE,Unicode (Big-Endian)
}

else
{
result = false;
//codepage = 0; //non BOM !
codepage = 932; //shift_jis,Japanese (Shift-JIS)
}

return result;
}

public static bool IsMatched(byte[] data, byte[] bom)
{
bool result = true;

for (int i = 0; i < bom.Length; i++)
{
if (bom[i] != data[i])
result = false;
}

return result;
}


public static bool JISEncodingJudgment(byte[] buffer, int sizeOfBuffer, out bool isJIS)
{
bool outOfSpecification = false;
bool esc1 = false;
bool esc2 = false;
byte[] byteESC1 = { 0x1B, 0x28, 0x42 };
byte[] byteESC2 = { 0x1B, 0x24, 0x42 };
byte[] backESC = { 0, 0, 0 };

// if ISO-2022-JP

for (int i = 0; i < sizeOfBuffer; i++)
{
if (0x80 <= buffer[i])
{
outOfSpecification = true;
break;
}
else
{
backESC[0] = backESC[1];
backESC[1] = backESC[2];
backESC[2] = buffer[i];
if (esc1 == false && IsMatched(backESC, byteESC1))
{
esc1 = true;
}
if (esc2 == false && IsMatched(backESC, byteESC2))
{
esc2 = true;
}
}
}

if (esc1 || esc2)
{
isJIS = true;
}
else
{
isJIS = false;
}

return outOfSpecification;
}

public static bool Utf8EncodingJudgment(byte[] buffer, int sizeOfBuffer)
{
bool outOfSpecification;

outOfSpecification = false;
uint[] byteChar = new uint[6];
int byteCharCount = 0;

for (int i = 0; i < sizeOfBuffer; i++)
{
//2バイト文字以上である
if (0x80 <= buffer[i])
{
//2バイト文字
uint char2byte = (uint)0b11100000 & (uint)buffer[i];
if (char2byte == 0b11000000)
{
//セカンドコード数が規格より少なければ規格外
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, false);
if (outOfSpecification)
{
break;
}

byteChar[0] = char2byte;
byteCharCount = 1;
continue;
}

//3バイト文字
uint char3byte = (uint)0b11110000 & (uint)buffer[i];
if (char3byte == 0b11100000)
{
//セカンドコード数が規格より少なければ規格外
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, false);
if (outOfSpecification)
{
break;
}

byteChar[0] = char3byte;
byteCharCount = 1;
continue;
}

//4バイト文字
uint char4byte = (uint)0b11111000 & (uint)buffer[i];
if (char4byte == 0b11110000)
{
//セカンドコード数が規格より少なければ規格外
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, false);
if (outOfSpecification)
{
break;
}

byteChar[0] = char4byte;
byteCharCount = 1;
continue;
}

//2バイト目以降のコード
uint charSecond = (uint)0b11000000 & (uint)buffer[i];
if (charSecond == 0b10000000)
{
// 文字の先頭がセカンドコードなら規格外
if (byteCharCount < 1)
{
outOfSpecification = true;
break;
}

//セカンドコードを保存
byteChar[byteCharCount] = charSecond;
byteCharCount++;

//セカンドコード数が規格より多ければ規格外
outOfSpecification = Utf8OutOfSpecification(byteChar[0], byteCharCount, true);
if (outOfSpecification)
{
break;
}

continue;
}

//どれにも当てはまらない
outOfSpecification = true;
break;
}
else
{
// 7bit文字
byteChar[0] = 0;
byteCharCount = 0;
}
}

return outOfSpecification;
}

public static bool Utf8OutOfSpecification(uint topByteChar, int byteCharCount, bool checkBig)
{
bool outOfSpecification = false;

//セカンドコード数が規格より多ければ規格外
if (topByteChar == 0b11000000)
{
if(checkBig == true)
{
if(byteCharCount > 2) outOfSpecification = true;
}
else
{
if (byteCharCount < 2) outOfSpecification = true;
}
}
else if (topByteChar == 0b11100000)
{
if (checkBig == true)
{
if (byteCharCount > 3) outOfSpecification = true;
}
else
{
if (byteCharCount < 3) outOfSpecification = true;
}
}
else if (topByteChar == 0b11110000)
{
if (checkBig == true)
{
if (byteCharCount > 4) outOfSpecification = true;
}
else
{
if (byteCharCount < 4) outOfSpecification = true;
}
}

return outOfSpecification;
}

public enum BYTECODE : byte { OneByteCode, TwoByteCode, KanaOneByte }

public static bool EUCJPEncodingJudgment(byte[] buffer, int sizeOfBuffer)
{
bool outOfSpecification = false;


BYTECODE beforeCode = BYTECODE.OneByteCode;
int byteCharCount = 0;

for (int i = 0; i < sizeOfBuffer; i++)
{
// 2バイトコード
if (0xA1 <= buffer[i] && buffer[i] <= 0xFE)
{
if(beforeCode == BYTECODE.KanaOneByte)
{
if(byteCharCount == 1)
{
byteCharCount = 2;
}
else
{
outOfSpecification = true;
break;
}
}

if (beforeCode == BYTECODE.TwoByteCode)
{
if (byteCharCount == 1)
byteCharCount = 2;
else if(byteCharCount == 2)
byteCharCount = 1;
}

beforeCode = BYTECODE.TwoByteCode;
}
// 1バイトコード
else if(buffer[i] <= 0x7F)
{
if (beforeCode == BYTECODE.TwoByteCode && byteCharCount == 1)
{
outOfSpecification = true;
break;
}

beforeCode = BYTECODE.OneByteCode;
byteCharCount = 1;
}
// 半角カタカナ2バイトコード
else if(buffer[i] == 0x8E && byteCharCount == 1)
{
beforeCode = BYTECODE.KanaOneByte;
byteCharCount = 1;
}
// あり得ない
else
{
outOfSpecification = true;
break;
}
}

return outOfSpecification;
}

public enum SJIS_BYTECODE : byte { OneByteCode, TwoByteCommon, TwoByteBefore, TwoByteAfter, KanaOneByte }

public static bool SJISEncodingJudgment(byte[] buffer, int sizeOfBuffer)
{
bool outOfSpecification;
SJIS_BYTECODE sjisByte = SJIS_BYTECODE.OneByteCode;

// if SJIS

outOfSpecification = false;

for (int i = 0; i < sizeOfBuffer; i++)
{
if (buffer[i] <= 0x7F)
{
sjisByte = SJIS_BYTECODE.OneByteCode;
}
else if (0xA1 <= buffer[i] && buffer[i] <= 0xDF)
{
sjisByte = SJIS_BYTECODE.KanaOneByte;
}
else if (0x81 <= buffer[i] && buffer[i] <= 0x9F)
{
sjisByte = SJIS_BYTECODE.TwoByteCommon;
}
else if (0xE0 <= buffer[i] && buffer[i] <= 0xEF)
{
if(sjisByte == SJIS_BYTECODE.TwoByteBefore)
{
outOfSpecification = true;
break;
}
sjisByte = SJIS_BYTECODE.TwoByteBefore;
}
else if (
(0x40 <= buffer[i] && buffer[i] <= 0x7E) ||
(0x80 <= buffer[i] && buffer[i] <= 0xFC)
)
{
if (sjisByte == SJIS_BYTECODE.TwoByteAfter)
{
outOfSpecification = true;
break;
}
sjisByte = SJIS_BYTECODE.TwoByteAfter;
}
else
{
outOfSpecification = true;
break;
}
}

return outOfSpecification;
}

}
}

 

コード解説

先に説明したようにこのプログラムは、

先にBOMの有無を判定する。

BOMの判定に使用している関数は、以前掲載したものと同じ「IsBOM」である。これの解説は省略する。

 

BOMが無ければ、規格の厳しい文字エンコーディングから、規格外になるケースが無いか検査し、規格外のケースが存在すれば、その文字エンコーディングでは無いと判定する。

最初に規格外のケースが存在しない文字エンコーディングが、「その文字エンコーディングである」と判定する。

文字エンコーディングの判定の順番は以下の順になる。

encoding , codename

us-ascii , US-ASCII
iso-2022-jp , Japanese (JIS)
utf-8 , Unicode (UTF-8)
euc-jp , Japanese (EUC)
shift_jis , Japanese (Shift-JIS)

最初に「FileStream」でバイナリモードでファイルを1000バイト読み込み、そのバイナリ値で文字エンコーディングを判定する。

判定処理は「EncodingJudgment」関数の中にまとまっている。

「EncodingJudgment」の中でそれぞれの文字エンコーディングの規格外を探す関数を呼び出す。

全て、規格外を検出したら、true を変えす。

false を返したら、全て規格に収まっていることを意味する。

 

内部の関数は以下になる。

 

JISEncodingJudgment

 ASCII か ISO-2022-JP の規格に全て当てはまるか検査する。

isJIS が true なら ISO-2022-JP を示し、false なら ASCII である事を示す。

 

文字コードが全て、0x80 未満であるか確認する。

また、JIS のエスケープシーケンスが存在するかどうかも判定している。

 

Utf8EncodingJudgment

 utf-8 の規格に全て当てはまるか検査する。

文字コードが全て、utf-8 の文字コードの「器」を持っているか判定する。

「器」に当てはまらない場合は true を返す。

「器」は二進数で以下の値だ。


1バイト文字(07ビット) : 0-xxxxxxx
2バイト文字(11ビット) : 110-xxxxx,10-xxxxxx
3バイト文字(16ビット) : 1110-xxxx,10-xxxxxx,10-xxxxxx
4バイト文字(21ビット) : 11110-xxx,10-xxxxxx,10-xxxxxx,10-xxxxxx

バイナリ全てにビットマスクを掛け、器と同じ値が取れるか判定する。

一度でも取れなければ true を返す。

 

EUCJPEncodingJudgment

 euc-jp の規格に全て当てはまるか検査する。

2バイトコードが一回しか登場しない、仮名コード「0x8E」の後に2バイトコードが登場しない、時に規格外と判定して true を返す。

 

SJISEncodingJudgment

 shift-jis の規格に全て当てはまるか検査する。

これは厳密な判定が難しく、0xE0 から 0xEF のコードが続いたら true 、

0x40 から 0x7E と 0x80 から 0xFC(0x81 から 0x9F を除く) が続いたら true と判定している。

他の文字エンコーディングに当てはまらない事が重要な判定基準しなる。

要するに消去法で shift-jis か否かを判定している。

 

このコードは例外的ケースに使うべき

以前から説明しているように、Windows 環境においては「BOM有りはUTF」「BOMなしはshift-jis」というルールで複数の文字エンコーディングのテキストファイルを運用すべきである。

しかし、何かの事情で文字コードの分からないテキストファイルや、BOMなしUTFを作ってしまう場合もある。

Windowsでも標準的に 「BOMなし UTF-8」 を使用するので、「BOMなし UTF-8」の発生を完全に防ぐのは難しい。

そこで、せめてBOMなしの shfit-jis とutf-8 を検出できるようにする必要があると考えこのコードサンプルを作った。

しかし、 shfit-jis の検出が難しく、検出する為には他の文字エンコーディングに当てはまらない事を検出する必要があり、このようなプログラムになった。

決して、 shfit-jis とutf-8 以外の文字エンコーディングも同時運用しろと言っているわけではない。

その点は誤解しないで欲しい。

あくまで、職場での文字エンコーディングは「BOM有りはUTF」「BOMなしはshift-jis」というルールで運用して欲しい。

安全のために。

 

shift-jis と utf-8 の混在問題に関する記事(リンクリスト)に戻る

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