在windows自帶的notepad(記事本)程序中輸入“聯(lián)通”兩個字,保存后再次打開,會發(fā)現(xiàn)“聯(lián)通”不見了,代之以“��ͨ”的亂碼。這是windows平臺上典型的中文編碼問題。即文件保存的時候是按照ANSI編碼(其實就是GB2312,后面會詳細(xì)介紹)保存,打開的時候程序按照UTF-8方式對內(nèi)容解釋,于是就出現(xiàn)了亂碼。避免亂碼的方式很簡單,在“文件”菜單中選擇“打開”命令,選擇保存的文件,然后選擇“ANSI”編碼,此時就能看到久違的“聯(lián)通”兩個字了。
在Linux平臺上如果使用cat等命令查看文件中的中文內(nèi)容時,可能出現(xiàn)亂碼。這也是編碼的問題。簡單的說是文件時按照A編碼保存,但是cat命令按照當(dāng)前Locale設(shè)定的B編碼去查看,在B和A不兼容的時候就出現(xiàn)了亂碼。
中文編碼由于歷史原因牽扯到不少標(biāo)準(zhǔn),在不了解的時候感覺一頭霧水;但其實理解編碼問題并不需要你深入了解各個編碼標(biāo)準(zhǔn),只要你明白了來龍去脈,了解了關(guān)鍵的知識點,就能分析和解決日常開發(fā)工作中碰到的大部分編碼問題。有感于我看過的資料和文章要么不夠全面,要么略顯枯燥,所以通過這篇文章記錄下筆者在日常工作中碰到的中文編碼原理相關(guān)問題,目的主要是自我總結(jié),如果能給讀者提供一些幫助那就算是意外之喜了。由于嚴(yán)謹(jǐn)?shù)木幋a標(biāo)準(zhǔn)對我來說是無趣的,枯燥的,難以記憶的,本文嘗試用淺顯易懂的生活語言解釋中文編碼相關(guān)的(也可能不相關(guān)的)一些問題,這也是為什么取名雜談的原因。本文肯定存在不規(guī)范不全面的地方,我會在參考資料里給出官方文檔的鏈接,也歡迎讀者在評論中提出更好的表達(dá)方式&指出錯誤,不勝感激。
對編碼問題的理解我認(rèn)為分為三個層次,第一個層次:概念,知道各個編碼標(biāo)準(zhǔn)的應(yīng)用場景,了解之間的差異,能分析和解決常見的一些編碼問題。第二個層次:標(biāo)準(zhǔn),掌握編碼的細(xì)節(jié),如編碼范圍,編碼轉(zhuǎn)換規(guī)則,知道這些就能自行開發(fā)編碼轉(zhuǎn)換工具。第三個層次,使用,了解中文的編碼2進(jìn)制存儲,在程序開發(fā)過程中選擇合理的編碼并處理中文。為了避免讓讀者陷入編碼標(biāo)準(zhǔn)的黑洞無法脫身(不相信?看看unicode的規(guī)范就明白我的意思了),同時由于編碼查詢&轉(zhuǎn)換工具等都有現(xiàn)成工具可以使用,本文只涉及第一個層次,不涉及第二層次,在第三層次上會做一些嘗試。在本文的最后提供了相關(guān)鏈接供對標(biāo)準(zhǔn)細(xì)節(jié)感興趣的同學(xué)繼續(xù)學(xué)習(xí)。最后,本文不涉及具體軟件的亂碼問題解決,如ssh,shell,vim,screen等,這些話題留給劍豪同學(xué)專文闡述。
電腦很聰明,可以幫我們做很多事情,最開始主要是科學(xué)計算,這也是為什么電腦別名計算機。電腦又很笨,在她的腦子里只有數(shù)字,即所有的數(shù)據(jù)在存儲和運算時都要使用二進(jìn)制數(shù)表示。這在最初電腦主要用來處理大量復(fù)雜的科學(xué)計算時不是什么大問題但是當(dāng)電腦逐步走入普通人的生活時,情況開始變遭了。辦公自動化等領(lǐng)域最主要的需求就是文字處理,電腦如何來表示文字呢?這個問題當(dāng)然難不倒聰明的計算機科學(xué)家們,用數(shù)字來代表字符唄。這就是“編碼”。
每個人都可以約定自己的一套編碼,只要使用方之間了解就ok了。比如說咱倆約定0×10表示a,0×11表示b。在一開始也的確是這樣的,出現(xiàn)了各式各樣的編碼。這樣有兩個問題:1.各個編碼的字符集不一樣,有的多,有的少。2.相同字符的編碼也不一樣。你這里a是0×10.他那里a可能是0×30。于是你保存的文件他就不能直接用,必須要轉(zhuǎn)換編碼。隨著溝通范圍的擴(kuò)大,采用不同編碼的人們互相通信就亂套了,這就是我們常說的:雞同鴨講。如果要避免這種混亂,那么大家就必須使用相同的編碼規(guī)則,于是美國有關(guān)的標(biāo)準(zhǔn)化組織就出臺了ASCII(American Standard Code for Information Interchange)編碼,統(tǒng)一規(guī)定了英文常用符號用哪些二進(jìn)制數(shù)來表示。ASCII是標(biāo)準(zhǔn)的單字節(jié)字符編碼方案,用于基于文本的數(shù)據(jù)。
ASCII最初是美國國家標(biāo)準(zhǔn),供不同計算機在相互通信時用作共同遵守的西文字符編碼標(biāo)準(zhǔn),已被國際標(biāo)準(zhǔn)化組織(International Organization for Standardization, ISO)定為國際標(biāo)準(zhǔn),稱為ISO 646標(biāo)準(zhǔn)。適用于所有拉丁文字字母。ASCII 碼使用指定的7 位或8 位二進(jìn)制數(shù)組合來表示128 或256 種可能的字符。標(biāo)準(zhǔn)ASCII 碼也叫基礎(chǔ)ASCII碼,使用7 位二進(jìn)制數(shù)來表示所有的大寫和小寫字母,數(shù)字0 到9、標(biāo)點符號, 以及在美式英語中使用的特殊控制字符。而最高位為1的另128個字符(80H―FFH)被稱為“擴(kuò)展ASCII”,一般用來存放英文的制表符、部分音標(biāo)字符等等的一些其它符號。
其中:0~31及127(共33個)是控制字符或通信專用字符(其余為可顯示字符),32~126(共95個)是字符(32是空格),其中48~57為0到9十個阿拉伯?dāng)?shù)字,65~90為26個大寫英文字母,97~122號為26個小寫英文字母,其余為一些標(biāo)點符號、運算符號等。

現(xiàn)在所有使用英文的電腦終于可以用同一種編碼來交流了。理解了ASCII編碼,其他字母型的語言編碼方案就觸類旁通了。
ASCII這種字符編碼規(guī)則顯然用來處理英文沒有什么問題,它的出現(xiàn)極大的促進(jìn)了信息在西方尤其是美國的傳播和交流。但是對于中文,常用漢字就有6000以上,ASCII 單字節(jié)編碼顯然是不夠用。為了粉碎美帝國主義通過編碼限制中國人民使用電腦的無恥陰謀,中國國家標(biāo)準(zhǔn)總局發(fā)布了GB2312碼即中華人民共和國國家漢字信息交換用編碼,全稱《信息交換用漢字編碼字符集――基本集》,1981年5月1日實施,通行于大陸。GB2312字符集中除常用簡體漢字字符外還包括希臘字母、日文平假名及片假名字母、俄語西里爾字母等字符,未收錄繁體中文漢字和一些生僻字。 EUC-CN可以理解為GB2312的別名,和GB2312完全相同。
GB2312是基于區(qū)位碼設(shè)計的,在區(qū)位碼的區(qū)號和位號上分別加上A0H就得到了GB2312編碼。這里第一次提到了“區(qū)位碼”,我就連帶把下面這幾個讓人摸不到頭腦的XX碼一鍋端了吧:
區(qū)位碼,國標(biāo)碼,交換碼,內(nèi)碼,外碼
區(qū)位碼:就是把中文常用的符號,數(shù)字,漢字等分門別類進(jìn)行編碼。區(qū)位碼把編碼表分為94個區(qū),每個區(qū)對應(yīng)94個位,每個位置就放一個字符(漢字,符號,數(shù)字都屬于字符)。這樣每個字符的區(qū)號和位號組合起來就成為該漢字的區(qū)位碼。區(qū)位碼一般用10進(jìn)制數(shù)來表示,如4907就表示49區(qū)7位,對應(yīng)的字符是“學(xué)”。區(qū)位碼中01-09區(qū)是符號、數(shù)字區(qū),16-87區(qū)是漢字區(qū),10-15和88-94是未定義的空白區(qū)。它將收錄的漢字分成兩級:第一級是常用漢字計3755個,置于16-55區(qū),按漢語拼音字母/筆形順序排列;第二級漢字是次常用漢字計3008個,置于56-87區(qū),按部首/筆畫順序排列。在網(wǎng)上搜索“區(qū)位碼查詢系統(tǒng)”可以很方便的找到漢字和對應(yīng)區(qū)位碼轉(zhuǎn)換的工具。為了避免廣告嫌疑和死鏈,這里就不舉例了。
國標(biāo)碼: 區(qū)位碼無法用于漢字通信,因為它可能與通信使用的控制碼(00H~1FH)(即0~31,還記得ASCII碼特殊字符的范圍嗎?)發(fā)生沖突。于是ISO2022規(guī)定每個漢字的區(qū)號和位號必須分別加上32(即二進(jìn)制數(shù)00100000,16進(jìn)制20H),得到對應(yīng)的國標(biāo)交換碼,簡稱國標(biāo)碼,交換碼,因此,“學(xué)”字的國標(biāo)交換碼計算為:
00110001 00000111+ 00100000 00100000 ------------------- 01010001 00100111
用十六進(jìn)制數(shù)表示即為5127H。
交換碼:即國標(biāo)交換碼的簡稱,等同上面說的國標(biāo)碼。
內(nèi)碼:由于文本中通常混合使用漢字和西文字符,漢字信息如果不予以特別標(biāo)識,就會與單字節(jié)的ASCII碼混淆。此問題的解決方法之一是將一個漢字看成是兩個擴(kuò)展ASCII碼,使表示GB2312漢字的兩個字節(jié)的最高位都為1。即國標(biāo)碼加上128(即二進(jìn)制數(shù)10000000,16進(jìn)制80H)這種高位為1的雙字節(jié)漢字編碼即為GB2312漢字的機內(nèi)碼,簡稱為內(nèi)碼。20H+80H=A0H。這也就是常說的在區(qū)位碼的區(qū)號和位號上分別加上A0H就得到了GB2312編碼的由來。
00110001 00000111+ 10100000 10100000 ------------------- 11010001 10100111
用十六進(jìn)制數(shù)表示即為D1A7H。
外碼:機外碼的簡稱,就是漢字輸入碼,是為了通過鍵盤字符把漢字輸入計算機而設(shè)計的一種編碼。 英文輸入時,相輸入什么字符便按什么鍵,外碼和內(nèi)碼一致。漢字輸入時,可能要按幾個鍵才能輸入一個漢字。 漢字輸入方案有成百上千個,但是這千差萬別的外碼輸入進(jìn)計算機后都會轉(zhuǎn)換成統(tǒng)一的內(nèi)碼。
最后總結(jié)一下上面的概念。中國國家標(biāo)準(zhǔn)總局把中文常用字符編碼為94個區(qū),每個區(qū)對應(yīng)94個位,每個字符的區(qū)號和位號組合起來就是該字符的區(qū)位碼, 區(qū)位碼用10進(jìn)制數(shù)來表示,如4907就表示49區(qū)7位,對應(yīng)的字符是“學(xué)”。 由于區(qū)位碼的取值范圍與通信使用的控制碼(00H~1FH)(即0~31)發(fā)生沖突。每個漢字的區(qū)號和位號分別加上32(即16進(jìn)制20H)得到國標(biāo)碼,交換碼。“學(xué)”的國標(biāo)碼為5127H。由于文本中通?;旌鲜褂脻h字和西文字符,為了讓漢字信息不會與單字節(jié)的ASCII碼混淆,將一個漢字看成是兩個擴(kuò)展ASCII碼,即漢字的兩個字節(jié)的最高位置為1,得到的編碼為GB2312漢字的內(nèi)碼?!皩W(xué)”的內(nèi)碼為D1A7H。無論你使用什么輸入法,通過什么樣的按鍵組合把“學(xué)”輸入計算機,“學(xué)”在使用GB2312(以及兼容GB2312)編碼的計算機里的內(nèi)碼都是D1A7H。
GB2312的出現(xiàn)基本滿足了漢字的計算機處理需要,但由于上面提到未收錄繁體字和生僻字,從而不能處理人名、古漢語等方面出現(xiàn)的罕用字,這導(dǎo)致了1995年《漢字編碼擴(kuò)展規(guī)范》(GBK)的出現(xiàn)。GBK編碼是GB2312編碼的超集,向下完全兼容GB2312,兼容的含義是不僅字符兼容,而且相同字符的編碼也相同,同時在字匯一級支持ISO/IEC10646―1和GB 13000―1的全部中、日、韓(CJK)漢字,共計20902字。GBK還收錄了GB2312不包含的漢字部首符號、豎排標(biāo)點符號等字符。CP936和GBK的有些許差別,絕大多數(shù)情況下可以把CP936當(dāng)作GBK的別名。
GB18030編碼向下兼容GBK和GB2312。GB18030收錄了所有Unicode3.1中的字符,包括中國少數(shù)民族字符,GBK不支持的韓文字符等等,也可以說是世界大多民族的文字符號都被收錄在內(nèi)。GBK和GB2312都是雙字節(jié)等寬編碼,如果算上和ASCII兼容所支持的單字節(jié),也可以理解為是單字節(jié)和雙字節(jié)混合的變長編碼。GB18030編碼是變長編碼,有單字節(jié)、雙字節(jié)和四字節(jié)三種方式。
其實,這三個標(biāo)準(zhǔn)并不需要死記硬背,只需要了解是根據(jù)應(yīng)用需求不斷擴(kuò)展編碼范圍即可。從GB2312到GBK再到GB18030收錄的字符越來越多即可。萬幸的是一直是向下兼容的,也就是說一個漢字在這三個編碼標(biāo)準(zhǔn)里的編碼是一模一樣的。這些編碼的共性是變長編碼,單字節(jié)ASCII兼容,對其他字符GB2312和GBK都使用雙字節(jié)等寬編碼,只有GB18030還有四字節(jié)編碼的方式。這些編碼最大的問題是2個。1.由于低字節(jié)的編碼范圍和ASCII有重合,所以不能根據(jù)一個字節(jié)的內(nèi)容判斷是中文的一部分還是一個獨立的英文字符。2.如果有兩個漢字編碼為A1A2B1B2,存在A2B1也是一個有效漢字編碼的特殊情況。這樣就不能直接使用標(biāo)準(zhǔn)的字符串匹配函數(shù)來判斷一個字符串里是否包含某一個漢字,而需要先判斷字符邊界然后才能進(jìn)行字符匹配判斷。
最后,提一個小插曲,上面講的都是大陸推行的漢字編碼標(biāo)準(zhǔn),使用繁體的中文社群中最常用的電腦漢字字符集標(biāo)準(zhǔn)叫大五碼(Big5),共收錄13,060個中文字,其中有二字為重覆編碼(實在是不應(yīng)該)。Big5雖普及于中國的臺灣、香港與澳門等繁體中文通行區(qū),但長期以來并非當(dāng)?shù)氐膰覙?biāo)準(zhǔn),而只是業(yè)界標(biāo)準(zhǔn)。倚天中文系統(tǒng)、Windows等主要系統(tǒng)的字符集都是以Big5為基準(zhǔn),但廠商又各自增刪,衍生成多種不同版本。2003年,Big5被收錄到臺灣官方標(biāo)準(zhǔn)的附錄當(dāng)中,取得了較正式的地位。這個最新版本被稱為Big5-2003。
看了上面的多個中文編碼是不是有點頭暈了呢?如果把這個問題放到全世界n多個國家n多語種呢?各國和各地區(qū)自己的文字編碼規(guī)則互相沖突的情況全球信息交換帶來了很大的麻煩。
要真正徹底解決這個問題,上面介紹的那些通過擴(kuò)展ASCII修修補補的方式已經(jīng)走不通了,而必須有一個全新的編碼系統(tǒng),這個系統(tǒng)要可以將中文、日文、法文、德文……等等所有的文字統(tǒng)一起來考慮,為每一個文字都分配一個單獨的編碼。于是,Unicode誕生了。Unicode(統(tǒng)一碼、萬國碼、單一碼)為地球上(以后會包括火星,金星,喵星等)每種語言中的每個字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,以滿足跨語言、跨平臺進(jìn)行文本轉(zhuǎn)換、處理的要求。在Unicode里,所有的字符被一視同仁,漢字不再使用“兩個擴(kuò)展ASCII”,而是使用“1個Unicode”來表示,也就是說,所有的文字都按一個字符來處理,它們都有一個唯一的Unicode碼。Unicode用數(shù)字0-0x10FFFF來映射這些字符,最多可以容納1114112個字符,或者說有1114112個碼位(碼位就是可以分配給字符的數(shù)字)。
提到Unicode不能不提UCS(通用字符集Universal Character Set)。UCS是由ISO制定的ISO 10646(或稱ISO/IEC 10646)標(biāo)準(zhǔn)所定義的標(biāo)準(zhǔn)字符集。UCS-2用兩個字節(jié)編碼,UCS-4用4個字節(jié)編碼。Unicode是由unicode.org制定的編碼機制,ISO與unicode.org是兩個不同的組織, 雖然最初制定了不同的標(biāo)準(zhǔn); 但目標(biāo)是一致的。所以自從unicode2.0開始, unicode采用了與ISO 10646-1相同的字庫和字碼, ISO也承諾ISO10646將不會給超出0x10FFFF的UCS-4編碼賦值, 使得兩者保持一致。大家簡單認(rèn)為UCS等同于Unicode就可以了。
在Unicode中:漢字“字”對應(yīng)的數(shù)字是23383。在Unicode中,我們有很多方式將數(shù)字23383表示成程序中的數(shù)據(jù),包括:UTF-8、UTF-16、UTF-32。UTF是“UCS Transformation Format”的縮寫,可以翻譯成Unicode字符集轉(zhuǎn)換格式,即怎樣將Unicode定義的數(shù)字轉(zhuǎn)換成程序數(shù)據(jù)。例如,“漢字”對應(yīng)的數(shù)字是0x6c49和0x5b57,而編碼的程序數(shù)據(jù)是:
BYTE data_utf8[] = {0xE6, 0xB1, 0x89, 0xE5, 0xAD, 0x97}; // UTF-8編碼WORD data_utf16[] = {0x6c49, 0x5b57}; // UTF-16編碼DWORD data_utf32[] = {0x6c49, 0x5b57}; // UTF-32編碼這里用BYTE、WORD、DWORD分別表示無符號8位整數(shù),無符號16位整數(shù)和無符號32位整數(shù)。UTF-8、UTF-16、UTF-32分別以BYTE、WORD、DWORD作為編碼單位?!皾h字”的UTF-8編碼需要6個字節(jié)。“漢字”的UTF-16編碼需要兩個WORD,大小是4個字節(jié)?!皾h字”的UTF-32編碼需要兩個DWORD,大小是8個字節(jié)。根據(jù)字節(jié)序的不同,UTF-16可以被實現(xiàn)為UTF-16LE或UTF-16BE,UTF-32可以被實現(xiàn)為UTF-32LE或UTF-32BE。
下面介紹UTF-8、UTF-16、UTF-32、BOM。
UTF-8
UTF-8以字節(jié)為單位對Unicode進(jìn)行編碼。從Unicode到UTF-8的編碼方式如下: