【字元編碼】徹底理解字元編碼

leesf發表於2016-03-25

一、前言

  在解決昨天的問題時,又引出了很多新的問題,如為什麼要進行編碼,這些編碼的關係如何,如ASCII,IOS-8859-1,GB2312,GBK,Unicode之間的關係,筆者想要徹底理解字元編碼背後的故事,遂進行了探索,具體筆記如下。如園友能讀完本篇文章,我相信會解開很多疑惑。

二、字元編碼

  2.1 為何需要編碼?

  我們知道,所有的資訊最終都表示為一個二進位制的字串,每一個二進位制位(bit)有0和1兩種狀態。當我們需要把字元'A'存入計算機時,應該對應哪種狀態呢,儲存時,我們可以將字元'A'用01000010(這個隨便編的)二進位制字串表示,存入計算機;讀取時,再將01000010還原成字元'A'。那麼問題來了,儲存時,字元'A'應該對應哪一串二進位制數呢,是01000010?或者是10000000 11110101?說白了,就是需要一個規則。這個規則可以將字元對映到唯一一種狀態(二進位制字串),這就是編碼。而最早出現的編碼規則就是ASCII編碼,在ASCII編碼規則中,字元'A'既不對應01000010,也不對應1000 0000 11110101,而是對應01000001(不要問為什麼,這是規則)。

  2.2 ASCII

  這套編碼規則是由美國定製,一共規定了128個字元的編碼,比如空格"SPACE"是32(十進位制)(二進位制00100000),大寫的字母A是65(二進位制01000001)。這128個符號(包括 32個不能列印出來的控制符號),只佔用了一個位元組(8 bit)的後面7位,最前面的1位統一規定為0。總共才有128個字元編碼,一個位元組都沒有用完,這好像似乎有點太少了。於是乎,就開始壓榨最高位,對其為1時也進行編碼,利用最高位進行編碼的方式就稱為非ASCII編碼,如ISO-8859-1編碼。

  2.3 ISO-8859-1

  這套編碼規則由ISO組織制定。是在 ASCII 碼基礎上又制定了一些標準用來擴充套件ASCII編碼,即 00000000(0) ~ 01111111(127) 與ASCII的編碼一樣,對 10000000(128) ~ 11111111(255)這一段進行了編碼,如將字元§編碼成 10100111(167)。ISO-8859-1編碼也是單位元組編碼,最多能夠表示256個字元。Latin1是ISO-8859-1的別名,有些環境下寫作Latin-1。但是,即使能夠表示256個字元,對中文而言,還是太少了,一個位元組肯定不夠,必須用多個位元組表示。但是,由於是單位元組編碼,和計算機最基礎的表示單位一致,所以很多時候,仍舊使用 ISO8859-1編碼來表示。而且在很多協議上,預設使用該編碼。比如,雖然"中文"兩個字不存在ISO8859-1編碼,以GB2312編碼為例,應該是D6D0 CEC4兩個字元,使用ISO8859-1編碼的時候則將它拆開為4個位元組來表示:D6D0 CEC4(事實上,在進行儲存的時候,也是以位元組為單位進行處理)。而如果是UTF編碼,則是6個位元組e4 b8 ad e6 96 87。很明顯,這種表示方法還需要以另一種編碼為基礎才能正確顯示。而常見的中文編碼方式有GB2312、BIG5、GBK。

  2.4 GB2312

  GB2312其對所收錄字元進行了"分割槽"處理,共94個區,區從1(十進位制)開始,一直到94(十進位制),每區含有94個位,位從1(十進位制)開始,一直到94(十進位制),共8836(94 * 94)個碼位,這種表示方式也稱為區位碼,GB2312是雙位元組編碼,其中高位元組表示區,低位元組表示位。各區具體說明如下:

01-09區收錄除漢字外的682個字元,有164個空位(9 * 94 - 682)。
10-15區為空白區,沒有使用。
16-55區收錄3755個一級漢字(簡體),按拼音排序。
56-87區收錄3008個二級漢字(簡體),按部首/筆畫排序。
88-94區為空白區,沒有使用。

  那麼根據區位碼如何算出GBK2312編碼呢?區位碼的表示範圍為0101 - 9494(包含了空的區位碼)。點選這裡,檢視中GB2312編碼區位碼。之後只需要按照如下規則進行轉化即可。

  1. 將區(十進位制)轉化為十六進位制。

  2. 將轉化的十六進位制加上A0,得到GB2312編碼的高位元組。

  3. 將位(十進位制)轉化為十六進位制。

  4. 將轉化的十六進位制加上A0,得到GB2312編碼的低位元組。

  5. 組合區和位,區在高位元組,位在低位元組。

  6. 得到GB2312編碼。

  具體的流程圖如下:

  例如:'李'字的區位碼為3278(表示在32區,78位)。1. 將32(區)轉化為十六進位制為20。2. 加上A0為C0。3. 將78(位)轉化為十六進位制為4E。4. 加上A0為EE。5. 組合區和位,為C0EE。6. 得到GB2312編碼,即'李'字的GB2312編碼為C0EE。

  GB2312用兩個位元組編碼,採用分割槽編碼,總共編碼的中文個數為6763(3755 + 3008)。這些漢字只是最常用的漢字,已經覆蓋中國大陸99.75%的使用頻率。但是,還有一些漢字在GB2312中沒有被編碼,如'鎔'字,在GB2312中就沒有被編碼,這樣就導致了問題,隨之就出現了主流的GBK編碼。在講解GBK編碼之前,我們另外講解一下BIG5編碼。

  2.5 BIG5

  BIG5採用雙位元組編碼,使用兩個位元組來表示一個字元。高位位元組使用了0x81-0xFE,低位位元組使用了0x40-0x7E,及0xA1-0xFE。該編碼是繁體中文字符集編碼標準,共收錄13060箇中文字,其中有二字為重複編碼,即“兀、兀”(A461及C94A)和“嗀、嗀”(DCD1及DDFC)。具體的分割槽如下:  

8140-A0FE 保留給使用者自定義字元(造字區)
A140-A3BF 標點符號、希臘字母及特殊符號。其中在A259-A261,收錄了度量衡單位用字:兙兛兞兝兡兣嗧瓩糎。
A3C0-A3FE 保留。此區沒有開放作造字區用。
A440-C67E 常用漢字,先按筆劃再按部首排序。
C6A1-F9DC 其它漢字。
F9DD-F9FE 製表符。

  點選這裡,檢視BIG5編碼。注意,BIG5編碼與GBK編碼沒有什麼關係。

  2.6 GBK

  GBK編碼擴充套件了GB2312,完全相容GB2312編碼(如'李'字的GBK、GB2312編碼均為C0EE),但其不相容BIG5編碼('長'字的BIG5編碼為AAF8,GBK編碼為E94C,'李'字的BIG5編碼為A7F5 不等於C0EE),即如果使用GB2312編碼,使用GBK解碼是完全正常的,但是如果使用BIG5編碼,使用GBK解碼,會出現亂碼。相比於GB2312編碼,GBK編碼了更多漢字,如'鎔'字。GBK編碼依然採用雙位元組編碼方案,其編碼範圍:8140-FEFE,剔除xx7F碼位,共23940個碼位。能表示 21003 個漢字。點選這裡,檢視GBK編碼。點選這裡,可以查詢中文的其他編碼。在GBK之後又出現了GB18030編碼,但是沒有形成主流,故不做介紹,至此,中文編碼的問題已經講解完成。那麼問題又來了,大陸網民與在海峽兩岸網民交流時,若都使用GBK編碼,則沒有問題,若一方使用GBK編碼,一方使用BIG5編碼,那麼就會出現亂碼問題,這是在海峽兩岸網民交流,如果漂洋過海進行交流呢?那就更容易出現亂碼問題,這時候我們可能想,要是有一套全世界都通用的編碼就好了,不要擔心,這樣的編碼確實是存在的,那就是Unicode。

  2.7 Unicode

  有兩個獨立的, 創立單一字符集的嘗試. 一個是國際標準化組織(ISO)的 ISO 10646 專案, 另一個是由多語言軟體製造商組成的協會組織的 Unicode 專案. 在1991年前後, 兩個專案的參與者都認識到, 世界不需要兩個不同的單一字符集. 它們合併雙方的工作成果, 併為創立一個單一編碼表而協同工作. 兩個專案仍都存在並獨立地公佈各自的標準, 但 Unicode 協會和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO 10646 標準的碼錶相容, 並緊密地共同調整任何未來的擴充套件。

  Unicode是指一張表,裡面包含了可能出現的所有字元,每個字元對應一個數字,這個數字稱為碼點(Code Point),如字元'H'的碼點為72(十進位制),字元'李'的碼點為26446(十進位制)。Unicode表包含了1114112個碼點,即從000000(十六進位制) - 10FFFF(十六進位制)。地球上所有字元都可以在Unicode表中找到對應的唯一碼點。點選這裡,查詢字元對應的碼點。Unicode將碼空間劃分為17個平面,從00 - 10(十六進位制,最高兩位),即從0 - 16(十進位制),每個平面有65536個碼點(2^16)其中最重要的是第一個Unicode平面(碼位從0000 - FFFF),包含了最常用的字元,該平面被稱為基本多語言平面(Basic Multilingual Plane),縮寫為BMP,其他平面稱為輔助平面(Supplementary Planes),在基本多文種平面內, 從D800到DFFF之間的碼位區段是永久保留不對映到字元的, 因此UTF-16編碼巧妙的利用了這保留下來的碼位來對輔助平面內的字元進行編碼,這點後面進行講解。Unicode只是一個符號集,只規定的字元所對應的碼點,並沒有指定如何儲存,如何進行儲存出現了不同的編碼方案,關於Unicode編碼方案主要有兩條主線:UCS和UTF。UTF主線由Unicode Consortium進行維護管理,UCS主線由ISO/IEC進行維護管理。

  2.8 UCS

  UCS全稱為"Universal Character Set",在UCS中主要有UCS-2和UCS-4。

  1. UCS-2

  UCS-2是定長位元組的,固定使用2個位元組進行編碼,從0000(十六進位制)- FFFF(十六進位制)的碼位範圍,對應第一個Unicode平面。採用BOM(Byte Order Mark)機制,該機制作用如下:1. 確定位元組流採用的是大端序還是小端序。2. 確定位元組流的Unicode編碼方案。

  2. UCS-4

  UCS-4是定長位元組的,固定使用4個位元組進行編碼。也採用了BOM機制。

  2.9 UTF

  UTF全稱為"Unicode Transformation Format",在UTF中主要有UTF-8,UTF-16和UTF-32。

  1. UTF-8

  UTF-8是一種變長編碼方式,使用1-4個位元組進行編碼。UTF-8完全相容ASCII,對於ASCII中的字元,UTF-8採用的編碼值跟ASCII完全一致。UTF-8是Unicode一種具體的編碼實現。UTF-8是在網際網路上使用最廣的一種Unicode的編碼規則,因為這種編碼有利於節約網路流量(因為變長編碼,而非統一長度編碼)。關於Unicode碼點如何轉化為UTF-8編碼,可以參照如下規則:

  ① 對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。

  ② 對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的unicode碼。

  總結的編碼規則如下:

      Unicode符號範圍                   |   UTF-8編碼方式
         (十六進位制) (十進位制)            |   (二進位制)
  ----------------------------------------------------------------------------------------------------
    0000 0000-0000 007F (0-127)           |    0xxxxxxx
    0000 0080-0000 07FF (128-2047)        |    110xxxxx 10xxxxxx
    0000 0800-0000 FFFF (2048-65535)      |     1110xxxx 10xxxxxx 10xxxxxx
    0001 0000-0010 FFFF (65536-1114111)   |    11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

  說明:字元'A'的Unicode碼點為65(十進位制),根據上表,在第一行範圍,則字元'A'的UTF-8編碼為01000001,中文字元'李'的Unicode碼點為26446(十進位制),二進位制為01100111 01001110,十六進位制為674E。根據上表,在第三行範圍,則將'李'二進位制程式碼從低位到高位依次填入x中,不足的填入0。得到UTF-8編碼為11100110 10011101 10001110,即E69D8E(十六進位制)。

  由上述編碼規則可知,0000 0000 - 0000 FFFF(第一行到第三行)為Unicode第一個平面(基本多語言平面),而0001 0000 - 10 FFFF(第四行)為Unicode其他平面(輔助平面)。在基本多語言平面對應了絕大多數常用的字元。對於大於65535(十進位制)的碼點,即在輔助平面上的碼點,需要使用4個位元組來進行UTF-8編碼。

  2. UTF-16

  UTF-8是不定長的編碼,使用1、2、3、4個位元組編碼,而UTF-16則只使用2或4個位元組編碼。UTF-16也是Unicode一種具體的編碼實現。關於Unicode如何轉化為UTf-16編碼規則如下

  ① 若Unicode碼點在第一平面(BPM)中,則使用2個位元組進行編碼。

  ② 若Unicode碼點在其他平面(輔助平面),則使用4個位元組進行編碼。

  關於輔助平面的碼點編碼更詳細解析如下:輔助平面碼點被編碼為一對16位元(四個位元組)長的碼元, 稱之為代理對(surrogate pair), 第一部分稱為高位代理(high surrogate)或前導代理(lead surrogates),碼位範圍為:D800-DBFF. 第二部分稱為低位代理(low surrogate)或後尾代理(trail surrogates), 碼位範圍為:DC00-DFFF。注意,高位代理的碼位從D800到DBFF,而低位代理的碼位從DC00到DFFF,總共恰好為D800-DFFF,這部分碼點在第一平面內是保留的,不對映到任何字元,所以UTF-16編碼巧妙的利用了這點來進行碼點在輔助平面內的4位元組編碼。

  說明:字元'A'的Unicode碼點為65(十進位制),十六進位制表示為41,在第一平面。根據規則,UTF-16採用2個位元組進行編碼。那麼問題又來了,知道了採用兩個位元組編碼,並且我們也知道計算機是以位元組為單位進行儲存,這兩個位元組應該表示為00 41(十六進位制)?或者是41 00(十六進位制)呢?這就引出了一個問題,需要用到之前提及的BOM機制來解決。

  表示為00 41意味著採用了大端序(Big endian),而表示為41 00意味著採用了小端序。那麼計算機如何知道儲存的字元資訊採用了大端序還是小端虛呢?這就需要加入一些控制資訊,具體是採用大端序,則在檔案前加入FE FF,採用小端序,則在檔案前加入FF FE。這樣,當計算開始讀取時發現前兩個位元組為FE FF,就表示之後的資訊採用的是小端序,反之,則是大端序。

  字元 (無法顯示,只能截圖顯示),其Unicode碼點為65902(十進位制),十六進位制為1016E,很顯然,已經超出了第一平面(BMP)所能表示的範圍。其在輔助平面內,根據規則,UTF-16採用4個位元組進行編碼。然而其編碼不是簡單擴充套件為4個位元組(00 01 01 6E),而是採用如下規則進行計算。

  ① 使用Unicode碼位減去100000(十六進位制),得到的值擴充套件20位(因為Unicode最大為10 FF FF(十六進位制),減去1 00 00(十六進位制)後,得到的結果最大為0FFF FF(十六進位制),即為20位,不足20位的,在高位加一個0,擴充套件至20位即可)。

  ② 將步驟一得到的20位,按照高十位和低十位進行分割。

  ③ 將步驟二的高十位擴充套件至2個位元組,再加上D800(十六進位制),得到高位代理或前導代理。取值範圍是D800 - 0xDBFF。

  ④ 將步驟二的低十位擴充套件至2個位元組,再加上DC00(十六進位制),得到低位代理或後尾代理。取值範圍是DC00 - 0xDFFF。

  Unicode轉UTF-16規則流程圖如下:

 

  按照這個規則,我們計算字元的UTF-16編碼,我們知道其碼點為1016E,減去10000得到016E,擴充套件至0016E,進行分割,得到高十位為00 0000 0000,十六進位制為0000,加上D800為D800;得到低十位為01 0110 1110,十六進位制為016E,加上DC00為DD6E;綜合得到D8 00 DD 6E。即UTF-16編碼為D8 00 DD 6E(也可為D8 0 DD 6E)。

  而對於UTF-32是使用4個位元組表示,也採用BOM機制,可以類比UTF-16,這裡不再額外介紹。

四、字元編碼區別

  4.1 UCS-2 與 UTF-16區別

  從上面的分析知道,UCS-2採用的兩個位元組進行編碼。在0000到FFFF的碼位範圍內,它和UTF-16基本一致,為什麼說基本一致,因為在UTF-16中從U+D800到U+DFFF的碼位不對應於任何字元,而在使用UCS-2的時代,U+D800到U+DFFF內的值被佔用。

  UCS-2只能表示BMP內的碼點(只採用2個位元組),而UTF-16可以表示輔助平面內的碼點(採用4個位元組)。

  我們可以抽象的認為UTF-16可看成是UCS-2的父集。在沒有輔助平面字元(surrogate code points)前,UTF-16與UCS-2所指的意思基本一致。但當引入輔助平面字元後,想要表示輔助平面字元時,就只能用UTF-16編碼了。

  4.2 UCS -4與 UTF-16的區別

  在BMP上,UTF-16採用2個位元組表示,而在輔助平面上,UTF-16採用的是4個位元組表示。對於UCS-4,不管在哪個平面都採用的是四個位元組表示。

  4.3 為什麼UTF-8編碼不需要BOM機制

  因為在UTF-8編碼中,其自身已經帶了控制資訊,如1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx,其中1110就起到了控制作用,所以不需要額外的BOM機制。

五、總結

  如果讀者有耐心看到這裡,我相信對於字元編碼這一塊已經就已經沒有什麼疑問了。寫到這裡,就完成了主流編碼的探索,探索的過程確實是不容易,最後弄清楚了,感覺相當的快樂,也把經驗總結分享給各位園友,如果讀者有任何疑問,歡迎交流,謝謝各位園友的觀看~

 

參考連結:

http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html#comment-text

http://www.joelonsoftware.com/articles/Unicode.html

http://blog.csdn.net/xys_777/article/details/5773763

http://www.zhihu.com/question/19817672

http://demon.tw/programming/utf-16-ucs-2.html

http://blog.csdn.net/dslztx/article/details/48830887

http://blog.csdn.net/dslztx/article/details/48947097

http://www.zhihu.com/question/22881537

http://blog.csdn.net/shangboerds/article/details/7498317

http://blog.csdn.net/shuilan0066/article/details/7865715

http://www.zhihu.com/question/23374078

http://swiftlet.net/archives/category/char-encoding

http://blog.csdn.net/shuilan0066/article/details/7839189

http://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/

http://www.freebuf.com/articles/others-articles/25623.html

http://blog.csdn.net/qinysong/article/details/1179513

http://unicode-table.com/cn/

相關文章