GBK(GB2312)向UTF-8的编码转换

时间:2021-05-13 14:06:32

最近做一个IE插件,要从网页中取得文字,编码到一个URL中去。在前一篇文章“中文URL编码”中,粗略地介绍了URL编码的规则,以及中文URL编码的过程,但在如何将GBK或者GB2312编码的汉字转换到UTF-8编码仍然是一个问题。编码是一个很复杂的问题,我也了解甚少,这里只是写写我的经验,欢迎补充和指正。

在PHP、.NET中,编码的转换都比较容易。ATL中有一些宏是用来做编码转换的,我没试过,而且我更愿意用后面所讲的方法。

在COM编程中,字符串多存储在BSTR结构中。网上许多文章都说这个数据结构中存储的就是Unicode,我就试了好多次从Unicode转UTF-8,未遂。在Debug的时候,含有中文字符串的BSTR能够正常显示,说明它的编码应该是GBK.

如何从GBK转换到UTF-8呢?libiconv应该可以做到,然而我使用它的Windows port后,可以编译、注册COM组件,就是工具栏出不来了,于是放弃。上网搜索,得到一个被广泛转载的CChineseCode类。然而它仅仅针对汉字(每个汉字在UTF-8编码中占3个字节),如果字符串中有英文,就有麻烦了,因为英文在UTF-8编码中只有一个字节。另外有的字符会占用更多的字节。所以这个类并不适用。

正确的方法是用Win32 API的MultiByteToWideCharWideCharToMultiByte两个函数,Wide character指的就是Unicode. GBK和UTF-8之间的转换,需要用Unicode作为桥梁(在这种方法里)。比如我们要转换这样一个字符串”编码 - Google 搜索”。

从GBK向Unicode转换

该字符串在BSTR类型的变量in中存储,首先将其转换为普通的字符串:

char *lpszText = _com_util::ConvertBSTRToString(in);

此时,如果用strlen函数取得lpszText的长度,则为18,4个汉字,每个占两个字节,另外有10个英文字符。所以说GBK/GB2312是MultiByte而不是WideChar. 并且有lpszText[0] == 0xb1 && lpszText[1] == 0xe0,在微软Windows Codepage 936这一页上查到果然是“编”字,更坚定了我们认为它是GBK的信心。

转换到Unicode所用的函数是MultiByteToWideChar,第一个参数是MultiByte的Code page,如果确定是GBK,就可以使用936. 我考虑它应该是与系统有关的(比如日语系统上应该是932),所以使用CP_ACP,系统所用的Codepage.

先通过将cchWideChar参数设置为0,取得转换后需要的空间大小,然后分配空间,再做实际的转换(转换时cbMultiByte为-1表示要转换的字符串以0结尾)。代码如下:

int wLen = MultiByteToWideChar(CP_ACP, 0, lpszText, -1, NULL, 0);
LPWSTR wStr = (LPWSTR)CoTaskMemAlloc(wLen * sizeof(WCHAR));
MultiByteToWideChar(CP_ACP, 0, lpszText, -1, wStr, wLen);

wLen是15,注意是指宽字符的个数,很贴心,14个字符,加上末尾的结束符。分配空间的时候也要注意,不是15个字节,而应该分配30个字节。这些在MSDN中都有说明,仔细看cchWideChar参数的介绍。最后一行代码执行后,wStr中就是这些汉字的Unicode了,查看一下,wStr[0] == 0×7f16,刚才在微软Windows Codepage 936查找时,“编”字的下面标明7f16,就是它的Unicode编码,说明一切正常。

从Unicode向UTF-8转换

转换到Unicode后,就可以使用WideCharToMultiByte函数将其转换到UTF-8编码,这次的code page要用CP_UTF8. 和前面的转换一样,先计算所需要的空间大小并分配,再做实际转换。

int aLen = WideCharToMultiByte(CP_UTF8, 0, wStr, -1, NULL, 0, NULL, NULL);
char* converted = (char*)CoTaskMemAlloc(aLen);
WideCharToMultiByte(CP_UTF8, 0, wStr, -1, converted, aLen, NULL, NULL);

aLen为23,因为4个汉字,每个占3个字节,加上10个英文字符(每个占1字节),再加末尾的’/0′,正好是23. 现在converted里就是字符串”编码 - Google 搜索”的UTF-8编码。converted[0] == 0xe7 && converted[1] == 0xbc,正是“编”字的UTF-8编码。

好了,现在终于得到了中英文混合字符串的UTF-8字节序列,可以进行URL编码(percent encoding)了。

如果你也看了CChineseCode类的代码,就会奇怪既然作者知道用WideCharToMultiByte做GB2312到Unicode的转换,为什么在UnicodeToUTF_8函数中要舍近求远呢?