我在解决乱码上面实际走了不少弯路,做了很多实验,查了很多资料。在这里做下笔记,希望后来者可以明白,少走些弯路。
从最熟悉的两种字符编码说起
除了一些旧的、没有考虑到兼容性的网页还在用gbk做编码外,大部分的网页都已经用utf-8做编码了。但是最令人头疼的是,windows的控制台是很不好显示utf-8的。有明君为我大C++写了两个函数,是正确的、好用的(除了用std::string做返回值让我等效率党有点觉得不爽之外……还是挺方便的).
#include <string>
#include <windows.h>
using std::string;
//gbk 转 utf8
string GBKToUTF8(const string& strGBK)
{
string strOutUTF8 = "";
WCHAR * str1;
int n = MultiByteToWideChar(CP_ACP, 0, strGBK.c_str(), -1, NULL, 0);
str1 = new WCHAR[n];
MultiByteToWideChar(CP_ACP, 0, strGBK.c_str(), -1, str1, n);
n = WideCharToMultiByte(CP_UTF8, 0, str1, -1, NULL, 0, NULL, NULL);
char * str2 = new char[n];
WideCharToMultiByte(CP_UTF8, 0, str1, -1, str2, n, NULL, NULL);
strOutUTF8 = str2;
delete[]str1;
str1 = NULL;
delete[]str2;
str2 = NULL;
return strOutUTF8;
}
//utf-8 转 gbk
string UTF8ToGBK(const string& strUTF8)
{
int len = MultiByteToWideChar(CP_UTF8, 0, strUTF8.c_str(), -1, NULL, 0);
unsigned short * wszGBK = new unsigned short[len + 1];
memset(wszGBK, 0, len * 2 + 2);
MultiByteToWideChar(CP_UTF8, 0, (LPCTSTR)strUTF8.c_str(), -1, wszGBK, len);
len = WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL);
char *szGBK = new char[len + 1];
memset(szGBK, 0, len + 1);
WideCharToMultiByte(CP_ACP,0, wszGBK, -1, szGBK, len, NULL, NULL);
//strUTF8 = szGBK;
std::string strTemp(szGBK);
delete[]szGBK;
delete[]wszGBK;
return strTemp;
}
这玩意儿不跨平台,因为它用到了windows api。我之所以把它放到跨平台编程上面来,是因为字符编码这东西只有到跨平台的时候才显得坑爹。
接着我是不是要介绍那俩函数一下?
int MultiByteToWideChar(
_In_ UINT CodePage, /*代码页是Windows下字符编码的叫法,gbk是936,utf-8是65001,CP_ACP是ANSI*/
_In_ DWORD dwFlags, /*选项标志,转换类型,设0就行了*/
_In_ LPCSTR lpMultiByteStr, /*多字节字符串*/
_In_ int cbMultiByte, /*字符串要处理的长度,如果是-1函数就会处理整个字符串*/
_Out_opt_ LPWSTR lpWideCharStr, /*输出的宽字符串缓存,如果为空就返回需要的宽字符串长度*/
_In_ int cchWideChar /*宽字符串缓存的长度,当然如果宽字符串为空,这个设0就可以了*/
);
int WideCharToMultiByte(
_In_ UINT CodePage,
_In_ DWORD dwFlags,
_In_ LPCWSTR lpWideCharStr,
_In_ int cchWideChar,
_Out_opt_ LPSTR lpMultiByteStr,
_In_ int cbMultiByte, /*前面的基本与MultiByteToWideChar都相同,就不解释了*/
_In_opt_ LPCSTR lpDefaultChar, /*填0即可*/
_Out_opt_ LPBOOL lpUsedDefaultChar /*填0即可*/
);
这两个函数分别是将多字节字符串转换为宽字符字符串 和 将宽字符字符串转换为多字节字符串(在此处晕倒的童鞋们我没有对不起你们……是M$那家伙对不起你们)。我早就说过Windows API 的界面不友好,这么多不知道干嘛吗用的参数,全部填0就对了。要是iconv(),它貌似只有4个参数,这才是好的榜样。
宽字符?多字节?
这是Windows给它们起的名字,让人摸不着头脑。
宽字符之所以叫做宽字符,是因为它是一个宽一点的字符。那什么是短字符……就是ascii了,1个字节1个字符绝对够短,而且只能表示256个西欧字符。宽字符呢,是2个字节1个字符。宽一点,但还是可以识别到一个字符是哪里的。而多字节呢,就是它在计算机里表示成多个字节,但是没有办法识别那里到那里是一个字符。
我不喜欢这两个函数的命名。如果按照Python的命名,MultiByteToWideChar 应该叫 decode(解码),WideCharToMultiByte 应该叫 encode(编码)。
所以呢?
如你所见,多字节无法准确识别字符的长度,处理起来就会很麻烦。而宽字符大多时候虽然比多字节多耗费一点空间,但是处理起来方便。比如正则表达式处理,引擎是基于字符去匹配的,宽字符可以两个字节两个字节跳着匹配,而多字节就会匹配错误。
比如有一个词“程序”=0xB3CCD0F2(gbk),我想匹配“绦”=0xCCD0(gbk),正则库会替我把中间那两个字节匹配了。用在C里用wchar_t,C++里用std::wstring,我们可以很准确的,无错误地匹配到我们想要的子串,因为引擎在迭代的时候是逐字(而不是逐字节)进行比较的。
1 >>> str1 = "绦"
2 >>> str2 = "程序"
3 >>> print re.findall(str1, str2)
4 ['\xcc\xd0']
5 >>> print re.findall(str1.decode("gbk"), str2.decode("gbk"))
6 []
所以在处理字符串的时候,但凡要处理中文,要先把用户给的字符串解码成Unicode。处理完之后显示出来或者保存,再编码成需要的charset。
Appendix
在不同的地方用不同的编码:
- 网络文本(如网页)传输一般用utf-8,因为有少量中文,而大部分是英文。
- 在保存为本地文件的时候,应该保存为Unicode,因为本地存储资源丰富,且可以节省时间,实时解码毕竟也是O(N^2)啊。
- 显示出来应该用系统的编码,中文Windows为gbk,繁体Windows为Big5,Linux一律为UTF-8。
- 源代码里的少量中文串尽量用"\x????\x????"来表示,如果有大量中文建议用gettext或者资源之类的以外挂的方式读入。
- Qt内部使用Unicode,所以编写Qt应用时显示文字直接传递宽字符串即可。
- NTFS的文件名、路径都是用
GBKUTF16LE编码的,所以如果Windows下用户输入的是路径就无需解码了。