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

使用Shift-JIS映射表转换,结果自然是正确的。

强行使用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文本使用BIG5映射表转换,正常的结果

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的字符把关。代码实现和算法略有出入,但基本一致。

2010年6月8日 | 归档于 程序
  1. 2011年8月13日 00:01 | #1

    过来,膜拜一下,也在搞编码转换的code,今天debug了一个。自感觉我被骗了。。。
    ms code page 上说 cp936 是gb2312。
    各种wiki shift jis gb2312 发现有很多地方不重合,于是直接判断值,跑起来一看,被骗了。
    cp 936 完全是 gb2312的超集。。。
    再学习一下。

发表评论

XHTML: 您可以使用这些标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
:wink: :-| :-x :twisted: :) 8-O :( :roll: :-P :oops: :-o :mrgreen: :lol: :idea: :-D :evil: :cry: 8) :arrow: :-? :?: :!: