はじめに
C#で日本語文字コードを本格的ではなく、簡易的に判定する方法です。
Encodingクラスで扱える日本語のエンコーディング名は、エンコーディングの一覧によると以下になります。
コード ページ | 名前 | 表示名 |
---|---|---|
932 | shift_jis | 日本語 (Shift-JIS) |
10001 | x-mac-日本語 | 日本語 (Mac) |
20290 | IBM290 | IBM EBCDIC (日本語カタカナ) |
20932 | EUC-JP | 日本語 (JIS 0208-1990 および 0212-1990) |
50220 | iso-2022-jp | 日本語 (JIS) |
50221 | csISO2022JP | 日本語 (JIS-1 バイトカタカナを許可) |
50222 | iso-2022-jp | 日本語 (JIS-1 バイトカタカナを許可する-SO/SI) |
51932 | euc-jp | 日本語 (EUC) |
この中に名前が同じものがありますが、@ITの記事によると名前からエンコーディングを取得すると
-
EUC-JP
→51932
-
iso-2022-jp
→50220
となるようです。今回したいことは簡易的な判定であるため、
-
EUC-JP
は51932
として判定します。 -
iso-2022-jp
は50220
として判定します。 -
10001 x-mac-日本語
と20290 IBM290
は判定しません。
判定方法としては、
- BOMで判定できるものはBOMで判定します。
-
エスケープシーケンスがあるものは
iso-2022-jp
と判定します。 - Encoding.GetStringとEncoding.GetBytesを利用し、あるエンコーディングと仮定し、バイト配列を文字列に変換、再度その文字列をバイト配列に変換したときに元のバイト配列になる、もしくは末尾を除いて元のバイト配列になる場合はそのエンコーディングとみなします。
- 複数のエンコーディングが想定される場合は日本語の文章らしさで判定します。
- 判定できなかった場合は
null
を返します。
としています。日本語の文章らしさについては数値の取り得る範囲から判断しています。
名前 | 戻り値 | 判定方法 |
---|---|---|
UTF-16BE | Encoding.BigEndianUnicode | BOMで判定 |
UTF-16LE | Encoding.Unicode | BOMで判定 |
UTF-8 with BOM | new UTF8Encoding(true, true) | BOMで判定 |
UTF-7 | Encoding.UTF7 | BOMで判定 |
UTF-32BE | new UTF32Encoding(true, true) | BOMで判定 |
UTF-32LE | new UTF32Encoding(false, true) | BOMで判定 |
iso-2022-jp | Encoding.GetEncoding(50220) | エスケープシーケンスで判定 |
Shift_JIS | Encoding.GetEncoding(932) | 再変換で判定 |
EUC-JP | Encoding.GetEncoding(51932) | 再変換で判定 |
UTF-8N | new UTF8Encoding(false, true) | 再変換で判定 |
判定不能 | null |
ソースコード
using System;
using System.IO;
using System.Text;
namespace Encodechecker
{
internal class Encodechecker
{
public static Encoding GetJpEncoding(string file, long maxSize = 50 * 1024)//ファイルパス、最大読み取りバイト数
{
try
{
if (!File.Exists(file))//ファイルが存在しない場合
{
return null;
}
else if (new FileInfo(file).Length == 0)//ファイルサイズが0の場合
{
return null;
}
else//ファイルが存在しファイルサイズが0でない場合
{
//バイナリ読み込み
byte[] bytes = null;
bool readAll = false;
using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
long size = fs.Length;
if (size <= maxSize)
{
bytes = new byte[size];
fs.Read(bytes, 0, (int)size);
readAll = true;
}
else
{
bytes = new byte[maxSize];
fs.Read(bytes, 0, (int)maxSize);
}
}
//判定
return GetJpEncoding(bytes, readAll);
}
}
catch
{
return null;
}
}
public static Encoding GetJpEncoding(byte[] bytes, bool readAll = false)
{
//BOM判定
var enc = checkBOM(bytes);
if (enc != null) return enc;
//簡易ISO-2022-JP判定
if (checkISO_2022_JP(bytes)) return Encoding.GetEncoding(50220);//iso-2022-jp
//簡易文字コード判定(再変換確認)
Encoding enc_sjis = Encoding.GetEncoding(932);//ShiftJIS
Encoding enc_euc = Encoding.GetEncoding(51932);//EUC-JP
Encoding enc_utf8_check = new UTF8Encoding(false, false);//utf8
Encoding enc_utf8 = new UTF8Encoding(false, true);//utf8
int sjis = checkReconversion(bytes, enc_sjis);
int euc = checkReconversion(bytes, enc_euc);
int utf8 = checkReconversion(bytes, enc_utf8_check);
//末尾以外は同一の場合は同一とみなす
if (utf8 >= bytes.Length - 3 && !readAll) utf8 = -1;
if (sjis >= bytes.Length - 1 && !readAll) sjis = -1;
if (euc >= bytes.Length - 1 && !readAll) euc = -1;
//同一のものが1つもない場合
if (sjis >= 0 && utf8 >= 0 && euc >= 0) return null;
//再変換で同一のものが1個だけの場合
if (sjis < 0 && utf8 >= 0 && euc >= 0) return enc_sjis;
if (utf8 < 0 && sjis >= 0 && euc >= 0) return enc_utf8;
if (euc < 0 && utf8 >= 0 && sjis >= 0) return enc_euc;
//同一のものが複数ある場合は日本語らしさ判定
double like_sjis = likeJapanese_ShiftJIS(bytes);
double like_euc = likeJapanese_EUC_JP(bytes);
double like_utf8 = likeJapanese_UTF8(bytes);
if (utf8 < 0 && sjis < 0 && euc < 0)
{
if (like_utf8 >= like_sjis && like_utf8 >= like_euc) return enc_utf8;
if (like_sjis >= like_euc) return enc_sjis;
return enc_euc;
}
else if (utf8 < 0 && sjis < 0)
{
return like_utf8 >= like_sjis ? enc_utf8 : enc_sjis;
}
else if (utf8 < 0 && euc < 0)
{
return like_utf8 >= like_euc ? enc_utf8 : enc_euc;
}
else if (euc < 0 && sjis < 0)
{
return like_sjis >= like_euc ? enc_sjis : enc_euc;
}
return null;
}
//BOM判定
private static Encoding checkBOM(byte[] bytes)
{
if (bytes.Length >= 2)
{
if (bytes[0] == 0xfe && bytes[1] == 0xff)//UTF-16BE
{
return Encoding.BigEndianUnicode;
}
else if (bytes[0] == 0xff && bytes[1] == 0xfe)//UTF-16LE
{
return Encoding.Unicode;
}
}
if (bytes.Length >= 3)
{
if (bytes[0] == 0xef && bytes[1] == 0xbb && bytes[2] == 0xbf)//UTF-8
{
return new UTF8Encoding(true, true);
}
else if (bytes[0] == 0x2b && bytes[1] == 0x2f && bytes[2] == 0x76)//UTF-7
{
return Encoding.UTF7;
}
}
if (bytes.Length >= 4)
{
if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xfe && bytes[3] == 0xff)//UTF-32BE
{
return new UTF32Encoding(true, true);
}
if (bytes[0] == 0xff && bytes[1] == 0xfe && bytes[2] == 0x00 && bytes[3] == 0x00)//UTF-32LE
{
return new UTF32Encoding(false, true);
}
}
return null;
}
//簡易ISO-2022-JP判定
private static bool checkISO_2022_JP(byte[] bytes)
{
string str = BitConverter.ToString(bytes);
if (str.Contains("1B-24-40")
|| str.Contains("1B-24-42")
|| str.Contains("1B-26-40-1B-24-42")
|| str.Contains("1B-24-28-44")
|| str.Contains("1B-24-28-4F")
|| str.Contains("1B-24-28-51")
|| str.Contains("1B-24-28-50")
|| str.Contains("1B-28-4A")
|| str.Contains("1B-28-49")
|| str.Contains("1B-28-42")
)
{
return true;
}
else
{
return false;
}
}
//簡易文字コード判定
//バイトを文字に変換し再度バイト変換したとき同一かどうか
private static int checkReconversion(byte[] bytes, Encoding enc)
{
try
{
//文字列に変換
string str = enc.GetString(bytes);
//バイトに再変換
byte[] rebytes = enc.GetBytes(str);
if (BitConverter.ToString(bytes) == BitConverter.ToString(rebytes))
{
return -1;//同一
}
else
{
int len = bytes.Length <= rebytes.Length ? rebytes.Length : bytes.Length;
for (int i = 0; i < len; i++)
{
if (bytes[i] != rebytes[i])
{
return i == 0 ? 0 : i - 1;//一致バイト数
}
}
}
}
catch
{
;
}
return 0;
}
//簡易日本語らしさ判定
//日本語の文章と仮定したときShiftJISらしいか
private static double likeJapanese_ShiftJIS(byte[] bytes)
{
int counter = 0;
bool judgeSecondByte = false; //次回の判定がShiftJISの2バイト目の判定かどうか
for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
if (!judgeSecondByte)
{
if ((b == 0x0D //CR
|| b == 0x0A //LF
|| b == 0x09 //tab
|| (0x20 <= b && b <= 0x7E)))//半角カナ除く1バイト
{
counter++;
}
else if ((0x81 <= b && b <= 0x9F) || (0xE0 <= b && b <= 0xFC))//ShiftJISの2バイト文字の1バイト目の場合
{
//2バイト目の判定を行う
judgeSecondByte = true;
}
else if (0xA1 <= b && b <= 0xDF)//ShiftJISの1バイト文字の場合(半角カナ)
{
;
}
else if (0x00 <= b && b <= 0x7F)//ShiftJISの1バイト文字の場合
{
;
}
else
{
//ShiftJISでない
return 0;
}
}
else
{
if ((0x40 <= b && b <= 0x7E) || (0x80 <= b && b <= 0xFC)) //ShiftJISの2バイト文字の2バイト目の場合
{
counter += 2;
judgeSecondByte = false;
}
else
{
//ShiftJISでない
return 0;
}
}
}
return (double)counter / (double)bytes.Length;
}
//日本語の文章と仮定したときUTF-8らしいか
private static double likeJapanese_UTF8(byte[] bytes)
{
string str = BitConverter.ToString(bytes) + "-";
int len = str.Length;
//日本語らしいものを削除
//制御文字
str = str.Replace("0D-", "");//CR
str = str.Replace("0A-", "");//LF
str = str.Replace("09-", "");//tab
//英数字記号
for (byte b = 0x20; b <= 0x7E; b++)
{
str = str.Replace(string.Format("{0:X2}-", b), "");
}
//ひらがなカタカナ
for (byte b1 = 0x81; b1 <= 0x83; b1++)
{
for (byte b2 = 0x80; b2 <= 0xBF; b2++)
{
str = str.Replace(string.Format("E3-{0:X2}-{1:X2}-", b1, b2), "");
}
}
//常用漢字
for (byte b1 = 0x80; b1 <= 0xBF; b1++)
{
for (byte b2 = 0x80; b2 <= 0xBF; b2++)
{
str = str.Replace(string.Format("E4-{0:X2}-{1:X2}-", b1, b2), "");
str = str.Replace(string.Format("E5-{0:X2}-{1:X2}-", b1, b2), "");
str = str.Replace(string.Format("E6-{0:X2}-{1:X2}-", b1, b2), "");
str = str.Replace(string.Format("E7-{0:X2}-{1:X2}-", b1, b2), "");
str = str.Replace(string.Format("E8-{0:X2}-{1:X2}-", b1, b2), "");
str = str.Replace(string.Format("E9-{0:X2}-{1:X2}-", b1, b2), "");
}
}
return ((double)len - (double)str.Length) / (double)len;
}
//日本語の文章と仮定したときEUC-JPらしいか
private static double likeJapanese_EUC_JP(byte[] bytes)
{
string str = BitConverter.ToString(bytes) + "-";
int len = str.Length;
//日本語らしいものを削除
//制御文字
str = str.Replace("0D-", "");//CR
str = str.Replace("0A-", "");//LF
str = str.Replace("09-", "");//tab
//英数字記号
for (byte b = 0x20; b <= 0x7E; b++)
{
str = str.Replace(string.Format("{0:X2}-", b), "");
}
//ひらがなカタカナ記号
for (byte b1 = 0xA1; b1 <= 0xA5; b1++)
{
for (byte b2 = 0xA1; b2 <= 0xFE; b2++)
{
str = str.Replace(string.Format("{0:X2}-{1:X2}-", b1, b2), "");
}
}
//常用漢字
for (byte b1 = 0xB0; b1 <= 0xEE; b1++)
{
for (byte b2 = 0xA1; b2 <= 0xFE; b2++)
{
str = str.Replace(string.Format("{0:X2}-{1:X2}-", b1, b2), "");
}
}
return ((double)len - (double)str.Length) / (double)len;
}
}
}
使い方の例
using System.Text;
//通常の使い方(ファイルの先頭50kBで判定)
Encoding enc = Encodechecker.Encodechecker.GetJpEncoding(@"C:\work\sample.txt");
//ファイルの先頭100kBで判定
Encoding enc = Encodechecker.Encodechecker.GetJpEncoding(@"C:\work\sample.txt", 100 * 1024);
//バイト配列から直接判定
Encoding enc = Encodechecker.Encodechecker.GetJpEncoding(bytes);
補足
上記では、
-
エスケープシーケンスによる
iso-2022-jp
判定。 - エンコーディングの確認。
- 日本語の文章らしさの確認。
を1個ずつ順番に実施しています。分かりやすいですが、バイト配列へのアクセスを何度も行っているため冗長な処理になっています。上記処理を一括して行う場合についても記載しておきます。
using System;
using System.IO;
using System.Text;
namespace Encodechecker
{
internal class Encodechecker
{
public static Encoding GetJpEncoding(string file, long maxSize = 50 * 1024)//ファイルパス、最大読み取りバイト数
{
try
{
if (!File.Exists(file))//ファイルが存在しない場合
{
return null;
}
else if (new FileInfo(file).Length == 0)//ファイルサイズが0の場合
{
return null;
}
else//ファイルが存在しファイルサイズが0でない場合
{
//バイナリ読み込み
byte[] bytes = null;
bool readAll = false;
using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
long size = fs.Length;
if (size <= maxSize)
{
bytes = new byte[size];
fs.Read(bytes, 0, (int)size);
readAll = true;
}
else
{
bytes = new byte[maxSize];
fs.Read(bytes, 0, (int)maxSize);
}
}
//判定
return GetJpEncoding(bytes, readAll);
}
}
catch
{
return null;
}
}
public static Encoding GetJpEncoding(byte[] bytes, bool readAll = false)
{
int len = bytes.Length;
//BOM判定
if (len >= 2 && bytes[0] == 0xfe && bytes[1] == 0xff)//UTF-16BE
{
return Encoding.BigEndianUnicode;
}
else if (len >= 2 && bytes[0] == 0xff && bytes[1] == 0xfe)//UTF-16LE
{
return Encoding.Unicode;
}
else if (len >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)//UTF-8
{
return new UTF8Encoding(true, true);
}
else if (len >= 3 && bytes[0] == 0x2b && bytes[1] == 0x2f && bytes[2] == 0x76)//UTF-7
{
return Encoding.UTF7;
}
else if (len >= 4 && bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xfe && bytes[3] == 0xff)//UTF-32BE
{
return new UTF32Encoding(true, true);
}
else if (len >= 4 && bytes[0] == 0xff && bytes[1] == 0xfe && bytes[2] == 0x00 && bytes[3] == 0x00)//UTF-32LE
{
return new UTF32Encoding(false, true);
}
//文字コード判定と日本語の文章らしさをまとめて確認
//Shift_JIS判定用
bool sjis = true; //すべてのバイトがShift_JISで使用するバイト範囲かどうか
bool sjis_2ndbyte = false;//次回の判定がShift_JISの2バイト目の判定かどうか
bool sjis_kana = false; //かな判定用
bool sjis_kanji = false; //常用漢字判定用
int counter_sjis = 0; //Shift_JISらしさ
//UTF-8判定用
bool utf8 = true; //すべてのバイトがUTF-8で使用するバイト範囲かどうか
bool utf8_multibyte = false; //次回の判定がUTF-8の2バイト目以降の判定かどうか
bool utf8_kana_kanji = false;//かな・常用漢字判定用
int counter_utf8 = 0; //UTF-8らしさ
int counter_utf8_multibyte = 0;
//EUC-JP判定用
bool eucjp = true; //すべてのバイトがEUC-JPで使用するバイト範囲かどうか
bool eucjp_multibyte = false; //次回の判定がEUC-JPの2バイト目以降の判定かどうか
bool eucjp_kana_kanji = false;//かな・常用漢字判定用
int counter_eucjp = 0; //EUC-JPらしさ
int counter_eucjp_multibyte = 0;
for (int i = 0; i < len; i++)
{
byte b = bytes[i];
//Shift_JIS判定
if (sjis)
{
if (!sjis_2ndbyte)
{
if (b == 0x0D //CR
|| b == 0x0A //LF
|| b == 0x09 //tab
|| (0x20 <= b && b <= 0x7E))//ASCII文字
{
counter_sjis++;
}
else if ((0x81 <= b && b <= 0x9F) || (0xE0 <= b && b <= 0xFC))//Shift_JISの2バイト文字の1バイト目の場合
{
//2バイト目の判定を行う
sjis_2ndbyte = true;
if (0x82 <= b && b <= 0x83)//Shift_JISのかな
{
sjis_kana = true;
}
else if ((0x88 <= b && b <= 0x9F) || (0xE0 <= b && b <= 0xE3) || b == 0xE6 || b == 0xE7)//Shift_JISの常用漢字
{
sjis_kanji = true;
}
}
else if (0xA1 <= b && b <= 0xDF)//Shift_JISの1バイト文字の場合(半角カナ)
{
;
}
else if (0x00 <= b && b <= 0x7F)//ASCIIコード
{
;
}
else
{
//Shift_JISでない
counter_sjis = 0;
sjis = false;
}
}
else
{
if ((0x40 <= b && b <= 0x7E) || (0x80 <= b && b <= 0xFC))//Shift_JISの2バイト文字の2バイト目の場合
{
if (sjis_kana && 0x40 <= b && b <= 0xF1)//Shift_JISのかな
{
counter_sjis += 2;
}
else if (sjis_kanji && 0x40 <= b && b <= 0xFC && b != 0x7F)//Shift_JISの常用漢字
{
counter_sjis += 2;
}
sjis_2ndbyte = sjis_kana = sjis_kanji = false;
}
else
{
//Shift_JISでない
counter_sjis = 0;
sjis = false;
}
}
}
//UTF-8判定
if (utf8)
{
if (!utf8_multibyte)
{
if (b == 0x0D //CR
|| b == 0x0A //LF
|| b == 0x09 //tab
|| (0x20 <= b && b <= 0x7E))//ASCII文字
{
counter_utf8++;
}
else if (0xC2 <= b && b <= 0xDF)//2バイト文字の場合
{
utf8_multibyte = true;
counter_utf8_multibyte = 1;
}
else if (0xE0 <= b && b <= 0xEF)//3バイト文字の場合
{
utf8_multibyte = true;
counter_utf8_multibyte = 2;
if (b == 0xE3 || (0xE4 <= b && b <= 0xE9))
{
utf8_kana_kanji = true;//かな・常用漢字
}
}
else if (0xF0 <= b && b <= 0xF3)//4バイト文字の場合
{
utf8_multibyte = true;
counter_utf8_multibyte = 3;
}
else if (0x00 <= b && b <= 0x7F)//ASCIIコード
{
;
}
else
{
//UTF-8でない
counter_utf8 = 0;
utf8 = false;
}
}
else
{
if (counter_utf8_multibyte > 0)
{
counter_utf8_multibyte--;
if (b < 0x80 || 0xBF < b)
{
//UTF-8でない
counter_utf8 = 0;
utf8 = false;
}
}
if (utf8 && counter_utf8_multibyte == 0)
{
if (utf8_kana_kanji)
{
counter_utf8 += 3;
}
utf8_multibyte = utf8_kana_kanji = false;
}
}
}
//EUC-JP判定
if (eucjp)
{
if (!eucjp_multibyte)
{
if (b == 0x0D //CR
|| b == 0x0A //LF
|| b == 0x09 //tab
|| (0x20 <= b && b <= 0x7E))//ASCII文字
{
counter_eucjp++;
}
else if (b == 0x8E || (0xA1 <= b && b <= 0xA8) || b == 0xAD || (0xB0 <= b && b <= 0xFE))//2バイト文字の場合
{
eucjp_multibyte = true;
counter_eucjp_multibyte = 1;
if (b == 0xA4 || b == 0xA5 || (0xB0 <= b && b <= 0xEE))
{
eucjp_kana_kanji = true;
}
}
else if (b == 0x8F)//3バイト文字の場合
{
eucjp_multibyte = true;
counter_eucjp_multibyte = 2;
}
else if (0x00 <= b && b <= 0x7F)//ASCIIコード
{
;
}
else
{
//EUC-JPでない
counter_eucjp = 0;
eucjp = false;
}
}
else
{
if (counter_eucjp_multibyte > 0)
{
counter_eucjp_multibyte--;
if (b < 0xA1 || 0xFE < b)
{
//EUC-JPでない
counter_eucjp = 0;
eucjp = false;
}
}
if (eucjp && counter_eucjp_multibyte == 0)
{
if (eucjp_kana_kanji)
{
counter_eucjp += 2;
}
eucjp_multibyte = eucjp_kana_kanji = false;
}
}
}
//ISO-2022-JP
if (b == 0x1B)
{
if ((i + 2 < len && bytes[i + 1] == 0x24 && bytes[i + 2] == 0x40) //1B-24-40
|| (i + 2 < len && bytes[i + 1] == 0x24 && bytes[i + 2] == 0x42) //1B-24-42
|| (i + 2 < len && bytes[i + 1] == 0x28 && bytes[i + 2] == 0x4A) //1B-28-4A
|| (i + 2 < len && bytes[i + 1] == 0x28 && bytes[i + 2] == 0x49) //1B-28-49
|| (i + 2 < len && bytes[i + 1] == 0x28 && bytes[i + 2] == 0x42) //1B-28-42
|| (i + 3 < len && bytes[i + 1] == 0x24 && bytes[i + 2] == 0x48 && bytes[i + 3] == 0x44) //1B-24-48-44
|| (i + 3 < len && bytes[i + 1] == 0x24 && bytes[i + 2] == 0x48 && bytes[i + 3] == 0x4F) //1B-24-48-4F
|| (i + 3 < len && bytes[i + 1] == 0x24 && bytes[i + 2] == 0x48 && bytes[i + 3] == 0x51) //1B-24-48-51
|| (i + 3 < len && bytes[i + 1] == 0x24 && bytes[i + 2] == 0x48 && bytes[i + 3] == 0x50) //1B-24-48-50
|| (i + 5 < len && bytes[i + 1] == 0x26 && bytes[i + 2] == 0x40 && bytes[i + 3] == 0x1B && bytes[i + 4] == 0x24 && bytes[i + 5] == 0x42)//1B-26-40-1B-24-42
)
{
return Encoding.GetEncoding(50220);//iso-2022-jp
}
}
}
// すべて読み取った場合で、最後が多バイト文字の途中で終わっている場合は判定NG
if (readAll)
{
if (sjis && sjis_2ndbyte)
{
sjis = false;
}
if (utf8 && utf8_multibyte)
{
utf8 = false;
}
if (eucjp && eucjp_multibyte)
{
eucjp = false;
}
}
if (sjis || utf8 || eucjp)
{
//日本語らしさの最大値確認
int max_value = counter_eucjp;
if (counter_sjis > max_value)
{
max_value = counter_sjis;
}
if (counter_utf8 > max_value)
{
max_value = counter_utf8;
}
//文字コード判定
if (max_value == counter_utf8)
{
return new UTF8Encoding(false, true);//utf8
}
else if (max_value == counter_sjis)
{
return Encoding.GetEncoding(932);//ShiftJIS
}
else
{
return Encoding.GetEncoding(51932);//EUC-JP
}
}
else
{
return null;
}
}
}
}