最近稍微研究了一下漢字的幾種編碼方式,感覺收穫頗多,在此羅列出來,以供需要的朋友參考。因為網上討論此話題的文章也頗多,我的一點見解似乎早就被前輩們覆蓋到,所以,本文就當是寫給自己的小小的總結吧。
GB2312似乎是最普通的編碼方式。80年代誕生,收錄6000多漢字。幾乎所有的中文系統都會支援GB2312。GB2312的編碼方式和古老的區位碼頗有淵源。漢字的區位碼每兩位加上0xA0就是電腦中的GB2312碼。比如“啊”區位碼是1601,GB碼是0xB0A1。B0 = 16 + A0,A1 = 01 + A0。幾乎所有的關於GB2312的資料都會說,它使用兩個位元組來存放漢字和符號。我對此頗為懷疑。在我的印象裡,GB碼似乎都是DBCS(double-byte character sets),即同時使用1個位元組和2個位元組兩種方式來表示一個字元。不知道是不是我的理解問題。在Windows中有一個小程式“字元對應表”。這是個很有用的東西。通過它尋找,Windows中,中文簡體字元集的編碼是同時用1個位元組和2個位元組來表示的。當高位是0x00~0x7f時,為一個位元組,高位為0x80以上時用2個位元組表示。
GBK是GB2312的擴充方案,使用了原來編碼空間的一些空白,增加了一些漢字,因此向下相容GB2312。是Windows中文系統的預設字元集。可以用Windows的記事本看一看GBK的編碼。開啟記事本寫幾個字:“在CSDN寫Blog”。檔案類型為“ANSI”。注意ANSI是英語文字的編碼方式(美國國家標準麼),可不是漢字的。這裡“ANSI”的意思就是採用系統預設的字元編碼方式編碼。用一個16進位編輯器開啟儲存的檔案,內容是:D4 DA 43 53 44 4E D0 B4 42 6C 6F 67 分析一下:
| D4 |
DA |
43 |
53 |
44 |
4E |
D0 |
B4 |
42 |
6C |
6F |
67 |
| 在 |
C |
S |
D |
N |
寫 |
B |
l |
o |
g |
很明顯單位元組和雙位元組是一起使用的。用過DOS的朋友都會記得漢字的亂碼。若將上面每一個位元組按照ASCII對應為字元,就可以看到亂碼了:╘┌CSDN╨┤Blog因為ASCII擴充中0x80以上的字元都是非英語字母和定位字元。因此漢字亂碼看上去總是那麼怪怪的。DBCS總會碰到的一個問題就是錯位。相信每個人都曾在網頁上見到過一小片一小片的亂碼。因為每一個漢字(準確的說是全形符號)都是兩個位元組拼成的。若是丟掉了其中一個位元組,那麼顯示就會出現錯亂。比如上面這一行字,我把開頭的D4刪掉,變成了:贑SDN寫Blog因為第二個位元組0xDA仍然大於0x7f,因此系統將0xDA43當作一個漢字輸出了(可以查一下字元對應表證實一下),而剩下的“SDN”照常輸出。若這是一長串的漢字沒有任何一個半形字元做“緩衝”的話,就要亂成一片了。
GB18030國內最新的編碼方案。支援了更多的字元,甚至包括了蒙文,藏文之類。GB18030的獨特之處在於它向下相容了GBK,但又擴充了編碼空間。GB18030採用1位元組,2位元組,4位元組三種方式來編碼。其中:1位元組從0x00~0x7f;2位元組的高端從0x81~0xfe,低端從0x40到0x7e,以及0x80到0xfe;4位元組從0x81308130~0xfe39fe39。GB18030有一個非常龐大的編碼空間,幾乎覆蓋了現在所有編碼方式的字元。它的4位元組編碼方式因為太過獨特,似乎給微軟造成了不少麻煩。現在WinXP和Win2K可以通過Add-Ones支援GB18030,它的字碼頁是54936,然而因為這些系統平台都不能支援4位元組的碼頁(Code Page),因此核心仍然是GBK的,GB18030隻是空有一個字碼頁而已。要想讓這些系統支援GB18030,需要改寫系統底層的許多代碼。在這些系統上,一些軟體會認為系統的預設編碼方式是GB18030(Java就是這樣)。但實際上系統預設碼頁仍然是GBK。這些系統雖然有18030的字型(宋體18030),但並不能處理所有GB18030的符號,因為系統並不支援4位元組的編碼。但據我編程證實,JDK 1.5可以支援GB18030的四位元組編碼。
Unicode國際統一的編碼方式。據我理解,似乎Unicode標準試圖用2個位元組覆蓋全世界所有的書寫符號,兩位元組總共65535個編碼位,據說現在還剩下3w多沒有編碼。前途無量,前途無量。Unicode是真正的純兩個位元組的編碼方案。所有的字元一視同仁,原有的ASCII字元通過在高位加00來相容Unicode。這使得Unicode成為非常危險的編碼方案。一旦某一個位元組丟失,其後的資訊將全部作廢。在Windows的記事本中儲存檔案時可以選擇Unicode以及Unicode Big Endian兩種方式。在這裡多說幾句。關於Big Endian和Little Endian的含義不想多說。為什麼Unicode要分大尾和小尾,而GBK不用?因為Unicode本身就是雙位元組碼,雙位元組是放在一起作為一個單元的。而GBK本質上講是單位元組的,每一個位元組是單獨處理的。因此不能分大尾和小尾。在字元對應表中查到“在”的代碼是0xD4DA,儲存後就是0xD4DA。“在CSDN寫Blog”儲存為Unicode編碼後,檔案內容是:FF FE 28 57 43 00 53 00 44 00 4E 00 99 51 42 00 6C 00 6F 00 67 00FF FE是Unicode碼串的頭,是固定的。不要以為是記事本這個軟體的自創。其後每兩個位元組是一個字元。應該注意到英文字元的編碼與ASCII是相容的。儲存為Unicode Big Endian後的檔案內容是:FE FF 57 28 00 43 00 53 00 44 00 4E 51 99 00 42 00 6C 00 6F 00 67編碼的順序和頭的樣子都變化了。試試刪掉一個位元組。小心Unicode的頭部不能動。以Little Endian的為例,刪掉FF FE後面的28,文字變成了:䍗勻䐀一餀䉑氀漀最完全沒有意義。
UTF關於UTF的含義,我看到了兩個不同的解釋。在http://www.dvpos.com/blog(周海漢的開發專欄)中的《深入剖析JSP和Servlet對中文的處理過程》(以下簡稱《深入》)一文中說是Unicode Text Format的縮寫。在另一個地方http://fmddlmyy.home4u.china.com中的《談談Unicode編碼,簡要解釋UCS、UTF、BMP、BOM等名詞》(以下簡稱《談談》)一文中說是UCS Transformation Format的縮寫。而UCS是Unicode Character Set的縮寫。感覺似乎後者更可能一些。從《談談》一文中得知,UTF-8的編碼規則為:
| Unicode |
UTF-8 |
| 0000 - 007F |
0xxxxxxx |
| 0080 - 07FF |
110xxxxx 10xxxxxx |
| 0800 – FFFF |
1110xxxx 10xxxxxx 10xxxxxx |
《深入》一文中對於以上規則有如下的文字描述:1. 如果Unicode的16位字元的頭9位是0,則用一個位元組表示,這個位元組的首位是“0”,剩下的7位與原字元中的後7位相同,如“/u0034”(0000 0000 0011 0100),用“34” (0011 0100)表示;(與源Unicode字元是相同的);2. 如果Unicode的16位字元的頭5位是0,則用2個位元組表示,首位元組是“110”開頭,後面的5位與源字元中除去頭5個零後的最高5位相同;第二個位元組以“10”開頭,後面的6位與源字元中的低6位相同。如“/u025d”(0000 0010 0101 1101),轉化後為“c99d”(1100 1001 1001 1101);3. 如果不符合上述兩個規則,則用三個位元組表示。第一個位元組以“1110”開頭,後四位為源字元的高四位;第二個位元組以“10”開頭,後六位為源字元中間的六位;第三個位元組以“10”開頭,後六位為源字元的低六位;如“/u9da7”(1001 1101 1010 0111),轉化為“e9b6a7”(1110 1001 1011 0110 1010 0111);試試把“在CSDN寫Blog”轉化成UTF-8編碼,內容變成:EF BB BF E5 9C A8 43 53 44 4E E5 86 99 42 6C 6F 67原來的字頭FF FE根據規則變成了EF BB BF。即:原來:1111 1110 1111 1111(FF FE à FE FF,因為Little Endian)UTF:1110 1111 1011 1011 1011 1111,即EF BB BF再試試“在”的編碼,原來:0101 0111 0010 1000(“在”的Unicode碼是0x5728)UTF:1110 0101 1001 1100 1010 1000,即E5 9C A8。由此,驗證了UTF-8的編碼,而且注意到,UTF-8也被當成了單位元組碼一樣對待,因為並沒有大尾小尾的區別。對於漢字居多的文本來說,UTF-8將大部分是每個漢字三個位元組的情況,反而會增加了碼長。但UTF-8的一大好處就是避免了Unicode“一個爛魚壞得一鍋腥”的糟糕情況。因為UTF-8每一個字有嚴密的編碼規則,如果傳輸中丟掉了一個位元組,結果不會影響很多。比如我們的例子中,刪掉除頭之外第一個位元組E5,會使其後的9C A8都不符合編碼規則,這樣會識別出壞掉的字,丟掉它們之後,後面的“CSDN寫Blog”仍然會保留。
ISO-8859-1最後說一下ISO-8859-1是因為它根本就不是漢字編碼。據“維基百科”記載,ISO 8859是給除英語之外的其他字母文字做編碼規範的。它佔用ASCII碼的0xA0~0xFF,每個字元集擴充96個字元。ISO-8859(多了一個連字號)則是ISO 8859加上原有的ASCII中的字元構成的編碼規範。說到ISO-8859-1因為它是許多未漢化的西文作業系統的編碼,比如Linux。最主要的原因是我發現My Phone的預設編碼也是它。許多西文的資料庫等等也只認識8859規範。我們常接觸的就是8859-1,實際上8859這套規範很大,似乎有16套之多(ISO 8859-1~ISO 8859-16)。8859規範其實很簡單。相對於DOS時的ASCII,它只不過是另一種擴充ASCII的方式。回頭看一下DOS下的漢字亂碼(GBK部分有寫),在ISO-8859-1編碼方式下顯示漢字時的亂碼原理本質上和DOS時是一樣的,它們都是將漢字的雙位元組解釋成兩個單獨的ASCII字元,只不過兩者對於ASCII的擴充是不一樣的,因此亂碼的樣子看起來也不太一樣。但是在記憶體中,漢字的編碼並沒有丟失。只要改變一種編碼方式,真相就會大白。將“在CSDN寫Blog”,轉化為ISO-8859-1編碼顯示出來是:ÔÚCSDNдBlog這也是IE顯示一些中文網頁時出現的亂碼的種類之一。 P.S. 我用Java做這些編碼之間的轉換實驗時無意中發現Java支援GB18030的四位元組表示方法。在將把ISO-8859-1轉化後的例子直接getBytes()時,發現結果是:81 30 88 38 81 30 89 33 43 53 44 4e 81 30 88 34 81 30 85 37 42 6c 6f 67這些“81 30”實在讓我迷糊了半天。後來注意到GB18030的編碼方式才明白,81 30 88 38正好在GB18030四位元組編碼的範圍內。猜測GB18030使用擴充出的四位元組部分編碼這些西歐字元,而Java認為我的機器的預設編碼是GB18030,使用getBytes()時就採用了GB18030進行編碼,而將ISO-8859-1轉化出的那些西歐文字轉化到GB18030時,就產生了這些四位元組的編碼。 參考連結:維基百科:http://zh.wikipedia.org/wiki/首頁周海漢的開發專欄:http://www.dvpos.com/bloghttp://fmddlmyy.home4u.china.com 微軟網站的
New Chinese Encoding GB-18030:http://www.microsoft.com/globaldev/drintl/columns/015/default.mspx?gssnb=1