GBK、Shift-JIS、BIG5编码检测算法
字符串的编码检测需要使用自定义的映射表,使用系统自带的Codepage是不大可能有准确率的,系统Codepage会将它所有没定义的字符映射为空格。
GBK、Shift-JIS、BIG5的码表空间都是不连贯的,而它们的有效空间也不完全重合,这为检测编码类型提供了可能性。
检测算法:
1、建立字符映射表:将任一ANSI编码的所有字符做全映射,从0x00到0xFFFF都有Unicode字符对应,但需要注意的是没有定义的字符统统映射到Unicode的0xFFFD(共三个映射表,既可用于检测也可用于转换)。
2、预设字符串的编码是Shift-JIS。
3、使用Shift-JIS的映射表从字符串第一个字符开始检测直至最后一个字符。如果遇到有字符映射到0xFFFD,设置预设编码是GBK,立刻停止步骤3,跳至步骤4。
4、如果预设编码是GBK,使用GBK的映射表从字符串第一个字符开始检测直至最后一个字符。如果遇到有字符映射到0xFFFD,设置预设编码是BIG5,立刻停止步 骤4,跳至步骤5。
5、如果预设编码是BIG5,使用BIG5的映射表从字符串第一个字符开始检测直至最后一个字符。如果遇到有字符映射到0xFFFD,设置预设编码是未知编码,立刻停止步 骤5,跳至步骤6。
6、返回预设编码。
这个算法的编码检测优先度是Shift-JIS>GBK>BIG5,也即如果顺利通过当前检测,则跳过后面所有检测。事实上,有大量字符串是能通过所有检测的。例如只有一个字符的字符串,假设这个字符是0x8140,在三个编码当中,都不会映射到Unicode的0xFFFD,因此能通过所有检测。但这没意义了。设定了优先度后是为了告诉用户最可能的一种编码。
为什么设定Shift-JIS>GBK>BIG5?
Shift-JIS的码表空间是0x00-0x7F、0xA1-0xDF、0x8140-0xFC4B
GBK的码表空间是0x00-0x7F、0x8140-0xFEFE
BIG5的码表空间是0x00-0x7F、0x8140-0xFEFE
但双字节段(0x8140以上)都不是全部已定义,Shift-JIS在0x8140以上的有效字符数是7724,GBK是21885,BIG5是19782。
GBK的覆盖面最大,有效空间基本覆盖了Shift-JIS,因此一个字符串如果能通过Shift-JIS检测,也差不多能通过GBK检测。如果将GBK的优先度设得比Shift-JIS高,那么大量真正是Shift-JIS编码的字符串就压根没机会返回给用户了。从反方向看,GBK中存在数量庞大的字符Shift-JIS没定义,Shift-JIS是高度覆盖不住GBK的,一个GBK文本从概率上没那么容易检测成Shift-JIS。也即:如果一个文本的真正编码是Shift-JIS,那么优先使用Shift-JIS检测自然不会有问题;如果它是GBK,那么优先使用Shift-JIS检测也不大会返回Shift-JIS。因此Shift-JIS应当优先于GBK。
Shift-JIS和BIG5的关系的考虑也类似。
从转换日系音乐cue、日文小说的宅用途出发,也应当将Shift-JIS设置为最高。
下面三张图是是Shift-JIS编码的小说“文学少女”と死にたがりの道化的转换结果。左边是当作本地编码的处理结果,可以无视。右边才是转换结果。
(程序和文本下载:http://code.google.com/p/unicue/downloads/detail?name=Ansi2Unicode_1.01.zip)
强行使用GBK映射表转换,没有出现标记0xFFFD(0xFFFD:�),也即能通过GBK检测
强行使用Big5转换,出现0xFFFD标记,也即通不过BIG5检测。BIG5跟Shift-JIS的有效空间重合度没那么高,区分相对容易一点
另外一个GBK文本强行使用Shift-JIS转换的结果。很容易就出现了0xFFFD标记
那么GBK和BIG5的优先度应该谁高呢?这里就见仁见智了。GBK的字符数比BIG5多,从概率上GBK相对容易覆盖住BIG5,BIG5相对不容易覆盖住GBK。倘若采用Shift-JIS和GBK之间的比较方法,应该是BIG5的优先度比GBK高。但从实际情况来看,真正BIG5编码的文本强行使用GBK映射表转换比较容易出现0xFFFD标记,真正GBK编码的文本强行使用BIG5映射表转换反而不容易出现0xFFFD标记。
GBK文本强行使用BIG5映射表转换的结果,不容易出现0xFFFD标记
BIG5文本强行使用GBK映射表转换,很容易出现0xFFFD标记
现实结果和表面现象完全相反。如果一个文本的真正编码是GBK,那么优先使用GBK检测自然不会有问题;如果它是BIG5,那么优先使用GBK检测也不大会返回GBK。因此余倾向于GBK的优先度高于BIG5。
最后附上源代码:
#define CODETYPE_DEFAULT 0 #define CODETYPE_GBK 1 #define CODETYPE_BIG5 2 #define CODETYPE_SHIFTJIS 3 #define CODETYPE_UTF8 4 #define CODETYPE_UNICODE 5 #define GBKOFFSET 0x8140 #define BIG5OFFSET 0x8140 #define JIS0201OFFSET 0xA1 #define JIS0201LENGTH 63 #define JIS0208OFFSET 0x8140 typedef int CodeType; CodeType CheckCodeType(const char* AnsiStr,UINT length) { if (!AnsiStr) return CODETYPE_DEFAULT; if (length==0) return CODETYPE_DEFAULT; //判断是否是UTF-8编码 UINT i=0; BOOL isUTF8=TRUE; while (i<length) { ...... } if (isUTF8) return CODETYPE_UTF8; UINT JISMapLength,GBKMapLength,BIG5MapLength; char *JISMapBuffer,*GBKMapBuffer,*BIG5MapBuffer; //加载映射表 TCHAR path[MAX_PATH]; //最长260 GetModuleFileName(NULL, path, MAX_PATH); CString mapPath=CString(path); int position=mapPath.ReverseFind('\\'); mapPath=mapPath.Left(position); CString mapFolder=mapPath; CFile loadMap; mapPath=mapFolder+_T("\\jis2u-little-endian.map"); if (!loadMap.Open(mapPath,CFile::modeRead)) { loadMap.Close(); ::AfxMessageBox(_T("JIStoUnicode map loading error!")); return CODETYPE_DEFAULT; } JISMapLength=loadMap.GetLength(); JISMapBuffer=new char[JISMapLength]; loadMap.Read((void *)JISMapBuffer,JISMapLength); loadMap.Close(); mapPath=mapFolder+_T("\\gb2u-little-endian.map"); if (!loadMap.Open(mapPath,CFile::modeRead)) { loadMap.Close(); ::AfxMessageBox(_T("GBKtoUnicode map loading error!")); if (JISMapBuffer) delete []JISMapBuffer; return CODETYPE_DEFAULT; } GBKMapLength=loadMap.GetLength(); GBKMapBuffer=new char[GBKMapLength]; loadMap.Read((void *)GBKMapBuffer,GBKMapLength); loadMap.Close(); mapPath=mapFolder+_T("\\b2u-little-endian.map"); if (!loadMap.Open(mapPath,CFile::modeRead)) { loadMap.Close(); ::AfxMessageBox(_T("Big5toUnicode map loading error!")); if (JISMapBuffer) delete []JISMapBuffer; if (GBKMapBuffer) delete []GBKMapBuffer; return CODETYPE_DEFAULT; } BIG5MapLength=loadMap.GetLength(); BIG5MapBuffer=new char[BIG5MapLength]; loadMap.Read((void *)BIG5MapBuffer,BIG5MapLength); loadMap.Close(); //检测编码,顺序是Shift-JIS>GBK>BIG5 //如果通过前面编码检测,则跳过后面所有检测 //检查对应的Unicode字符是否为0xFFFD,文本越长,准确度越高 //语义分析之类是无理的了 CodeType strCodeType=CODETYPE_SHIFTJIS; unsigned char low=0,high=0; WCHAR chr=0; for (i=0;i<length;) { memcpy(&high,AnsiStr+i,1); //读取第一个byte i++; if (high<=0x7F) //ASCII码区 { low=high; high=0; } else if ((high>=0xA1)&&(high<=0xDF)) //半角片假名区 { low=high; high=0; } else //双字节区 { memcpy(&low,AnsiStr+i,1); //读取低位 i++; } chr=low+high*256; if (chr<0x80) // ASCII {} else if (chr<JIS0201OFFSET) // 0x80 - 0xA0 未定义空间 { strCodeType=CODETYPE_DEFAULT; // 未知编码 break; } else if (chr<(JIS0201OFFSET+JIS0201LENGTH)) // 0xA1 - 0xDF 半角假名区 {} else if (chr<JIS0208OFFSET) // 0xE0 - 0x813F 未定义空间 { strCodeType=CODETYPE_DEFAULT; // 未知编码 break; } else // 0x8140 - 0xFFFF { int offset; offset=chr-JIS0208OFFSET+JIS0201LENGTH; memcpy((void*)&chr,JISMapBuffer+offset*2,2); if (chr==0xFFFD) { strCodeType=CODETYPE_GBK; break; } } } if (strCodeType==CODETYPE_GBK) { for (i=0;i<length;) { memcpy(&high,AnsiStr+i,1); //读取第一个byte i++; if (high>0x7F) //第一个byte是高位 { memcpy(&low,AnsiStr+i,1); //读取低位 i++; } else { low=high; high=0; } chr=low+high*256; if (chr<0x80) // ASCII码 {} else if (chr<GBKOFFSET) // 0x80 - 0x813F 未定义空间 { strCodeType=CODETYPE_DEFAULT; // 未知编码 break; } else { int offset; offset=chr-GBKOFFSET; memcpy((void*)&chr,GBKMapBuffer+offset*2,2); if (chr==0xFFFD) { strCodeType=CODETYPE_BIG5; break; } } } } if (strCodeType==CODETYPE_BIG5) { for (i=0;i<length;) { memcpy(&high,AnsiStr+i,1); //读取第一个byte i++; if (high>0x7F) //第一个byte是高位 { memcpy(&low,AnsiStr+i,1); //读取低位 i++; } else { low=high; high=0; } chr=low+high*256; if (chr<0x80) // ASCII码 {} else if (chr<BIG5OFFSET) // 0x80 - 0x813F 未定义空间 { strCodeType=CODETYPE_DEFAULT; // 未知编码 break; } else { int offset; offset=chr-BIG5OFFSET; memcpy((void*)&chr,BIG5MapBuffer+offset*2,2); if (chr==0xFFFD) { strCodeType=CODETYPE_DEFAULT; break; } } } } if (JISMapBuffer) delete []JISMapBuffer; JISMapBuffer=NULL; if (GBKMapBuffer) delete []GBKMapBuffer; GBKMapBuffer=NULL; if (BIG5MapBuffer) delete []BIG5MapBuffer; BIG5MapBuffer=NULL; return strCodeType; }
映射表文件并没有从0x00持续到0xFFFF,而是采取了精简策略。
GBK和BIG5的映射表范围均是0x8140-0xFFFF(其中未定义的字符都映射到0xFFFD),转换是对0x80-0x813F的字符也都映射到0xFFFD,检测则是立刻退出。Shift-JIS的映射表由两段凑在一起,因此有两个偏移,范围是0xA1-0xDF和0x8140-0xFFFF,转换是对0x80-0xA0和0xE0-0x813F的字符也都映射到0xFFFD,检测是立刻退出。最后的还有一个检测会对0x8140-0xFFFF的字符把关。代码实现和算法略有出入,但基本一致。
过来,膜拜一下,也在搞编码转换的code,今天debug了一个。自感觉我被骗了。。。
ms code page 上说 cp936 是gb2312。
各种wiki shift jis gb2312 发现有很多地方不重合,于是直接判断值,跑起来一看,被骗了。
cp 936 完全是 gb2312的超集。。。
再学习一下。