我在解決亂碼上面實(shí)際走了不少?gòu)澛罚隽撕芏鄬?shí)驗(yàn),查了很多資料。在這里做下筆記,希望后來者可以明白,少走些彎路。
從最熟悉的兩種字符編碼說起
除了一些舊的、沒有考慮到兼容性的網(wǎng)頁(yè)還在用gbk做編碼外,大部分的網(wǎng)頁(yè)都已經(jīng)用utf-8做編碼了。但是最令人頭疼的是,windows的控制臺(tái)是很不好顯示utf-8的。有明君為我大C++寫了兩個(gè)函數(shù),是正確的、好用的(除了用std::string做返回值讓我等效率黨有點(diǎn)覺得不爽之外……還是挺方便的).
#include <string>
#include <windows.h>
using std::string;
//gbk 轉(zhuǎn) 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 轉(zhuǎn) 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;
}
這玩意兒不跨平臺(tái),因?yàn)樗玫搅藈indows api。我之所以把它放到跨平臺(tái)編程上面來,是因?yàn)樽址幋a這東西只有到跨平臺(tái)的時(shí)候才顯得坑爹。
接著我是不是要介紹那倆函數(shù)一下?
int MultiByteToWideChar(
_In_ UINT CodePage, /*代碼頁(yè)是Windows下字符編碼的叫法,gbk是936,utf-8是65001,CP_ACP是ANSI*/
_In_ DWORD dwFlags, /*選項(xiàng)標(biāo)志,轉(zhuǎn)換類型,設(shè)0就行了*/
_In_ LPCSTR lpMultiByteStr, /*多字節(jié)字符串*/
_In_ int cbMultiByte, /*字符串要處理的長(zhǎng)度,如果是-1函數(shù)就會(huì)處理整個(gè)字符串*/
_Out_opt_ LPWSTR lpWideCharStr, /*輸出的寬字符串緩存,如果為空就返回需要的寬字符串長(zhǎng)度*/
_In_ int cchWideChar /*寬字符串緩存的長(zhǎng)度,當(dāng)然如果寬字符串為空,這個(gè)設(shè)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即可*/
);
這兩個(gè)函數(shù)分別是將多字節(jié)字符串轉(zhuǎn)換為寬字符字符串 和 將寬字符字符串轉(zhuǎn)換為多字節(jié)字符串(在此處暈倒的童鞋們我沒有對(duì)不起你們……是M$那家伙對(duì)不起你們)。我早就說過Windows API 的界面不友好,這么多不知道干嘛嗎用的參數(shù),全部填0就對(duì)了。要是iconv(),它貌似只有4個(gè)參數(shù),這才是好的榜樣。
寬字符?多字節(jié)?
這是Windows給它們起的名字,讓人摸不著頭腦。
寬字符之所以叫做寬字符,是因?yàn)樗且粋€(gè)寬一點(diǎn)的字符。那什么是短字符……就是ascii了,1個(gè)字節(jié)1個(gè)字符絕對(duì)夠短,而且只能表示256個(gè)西歐字符。寬字符呢,是2個(gè)字節(jié)1個(gè)字符。寬一點(diǎn),但還是可以識(shí)別到一個(gè)字符是哪里的。而多字節(jié)呢,就是它在計(jì)算機(jī)里表示成多個(gè)字節(jié),但是沒有辦法識(shí)別那里到那里是一個(gè)字符。
我不喜歡這兩個(gè)函數(shù)的命名。如果按照Python的命名,MultiByteToWideChar 應(yīng)該叫 decode(解碼),WideCharToMultiByte 應(yīng)該叫 encode(編碼)。
所以呢?
如你所見,多字節(jié)無法準(zhǔn)確識(shí)別字符的長(zhǎng)度,處理起來就會(huì)很麻煩。而寬字符大多時(shí)候雖然比多字節(jié)多耗費(fèi)一點(diǎn)空間,但是處理起來方便。比如正則表達(dá)式處理,引擎是基于字符去匹配的,寬字符可以兩個(gè)字節(jié)兩個(gè)字節(jié)跳著匹配,而多字節(jié)就會(huì)匹配錯(cuò)誤。
比如有一個(gè)詞“程序”=0xB3CCD0F2(gbk),我想匹配“絳”=0xCCD0(gbk),正則庫(kù)會(huì)替我把中間那兩個(gè)字節(jié)匹配了。用在C里用wchar_t,C++里用std::wstring,我們可以很準(zhǔn)確的,無錯(cuò)誤地匹配到我們想要的子串,因?yàn)橐嬖诘臅r(shí)候是逐字(而不是逐字節(jié))進(jìn)行比較的。
1 >>> str1 = "絳"
2 >>> str2 = "程序"
3 >>> print re.findall(str1, str2)
4 ['\xcc\xd0']
5 >>> print re.findall(str1.decode("gbk"), str2.decode("gbk"))
6 []
所以在處理字符串的時(shí)候,但凡要處理中文,要先把用戶給的字符串解碼成Unicode。處理完之后顯示出來或者保存,再編碼成需要的charset。
Appendix
在不同的地方用不同的編碼:
- 網(wǎng)絡(luò)文本(如網(wǎng)頁(yè))傳輸一般用utf-8,因?yàn)橛猩倭恐形模蟛糠质怯⑽摹?/em>
- 在保存為本地文件的時(shí)候,應(yīng)該保存為Unicode,因?yàn)楸镜卮鎯?chǔ)資源豐富,且可以節(jié)省時(shí)間,實(shí)時(shí)解碼畢竟也是O(N^2)啊。
- 顯示出來應(yīng)該用系統(tǒng)的編碼,中文Windows為gbk,繁體Windows為Big5,Linux一律為UTF-8。
- 源代碼里的少量中文串盡量用"\x????\x????"來表示,如果有大量中文建議用gettext或者資源之類的以外掛的方式讀入。
- Qt內(nèi)部使用Unicode,所以編寫Qt應(yīng)用時(shí)顯示文字直接傳遞寬字符串即可。
- NTFS的文件名、路徑都是用
GBKUTF16LE編碼的,所以如果Windows下用戶輸入的是路徑就無需解碼了。