我們都知道,計算機只能處理數字,即0和1兩種狀態,如果要處理文本,就必須先把文本轉換為數字才能處理。這種將信息從一種形式轉換為另一種形式的過程叫做編碼。最早的計算機在設計時采用8個比特作為一個字節,計算機存儲的最小單位就是字節。一個字節可以組合出256種不同的狀態,每一個狀態對應一個符號,也就是256個符號
字符編碼 = 字符集 + 編碼規則
ASCII
由於計算機是美國人發明的,所以只出現大小寫英文字母、數字和一些符號。因此,最早只有128個字符被編碼到計算機里,編號0-127稱為碼位(碼位代表對應字符的一個ID信息),而這128個字符集合稱為ASCII字符集
有了字符集以后,就要考慮如何將字符集里的字符存儲到計算機中。美國人直接將每個字符的碼位轉換成二進制信息進行存儲,而轉換后的二進制碼就叫做ASCII碼。所以ASCII碼只能表示從0-127,共計128個字符。這128個字符只使用了8位二進制數中的后面7位,最前面的位統一規定為0
ASCII 字符集對照表
碼位 | 字符 | ASCII碼 |
---|---|---|
0 | 空字符 | 00000000 |
1 | 標題開始 | 00000001 |
2 | 正文開始 | 00000010 |
… | … | … |
10 | 換行 | 00001010 |
… | … | … |
13 | 回車 | 00001101 |
… | … | … |
31 | 單元分隔符 | 00011111 |
32 | 空格 | 00100000 |
33 | ! | 00100001 |
… | … | … |
48 | 0 | 00110000 |
… | … | … |
57 | 9 | 00111001 |
… | … | … |
65 | A | 01000001 |
… | … | … |
90 | Z | 01011010 |
… | … | … |
97 | a | 01100001 |
… | … | … |
122 | z | 01111010 |
… | … | … |
126 | ~ | 01111110 |
127 | 刪除 | 01111111 |
歐洲編碼
英語用128個符號編碼足夠使用了,但對於其他語言來說是不夠的。於是歐洲就在原有的ASCII碼基礎上進行了擴展,利用字節中閑置的最高位編入新的符號(將原來的第一位0變成了1),這樣一來就可以在采用單字節編碼的同時表示256個符號。新增的這128個字符(碼位128-255)叫做擴展ASCII字符集,對應的ASCII碼就叫做擴展ASCII碼
中國編碼
當電腦來到中國以后發現,擴展后的編碼也就256個,而中國文字多達上萬,肯定是不夠用的。既然一個字節只能表示256種符號,那就必須使用多個字節表達一個符號
GB2312字符集:采用分區管理,共計94個區,每個區包含94個位,共8836個碼位
- 01-09區收錄除漢字外的682個字符
- 10-15區為空白區,沒有使用
- 16-55區收錄3755個一級漢字,按拼音排序(常用漢字)
- 56-87區收錄3008個二級漢字,按部首/筆畫排序(不常用漢字)
- 88-94區為空白區,沒有使用
字符集舉例
- 英文字符a處於03區的第6行第5列,那它的碼位就是0365
- 中文字符"餅"處於17區第9行第3列,那它的碼位就是1793
- 中文字符“侃”處於57區第0行第9列,那它的碼位就是5709
編碼存儲方式(這里以“侃”為例)
- 一個字符的碼位按前兩位和后兩位分開:將5709分為57和09
- 把分開的兩個數轉換為十六進制:0x39和0x09
- 兩個十六進制分別加上0xA0:0x39加上0xA0得到0xD9;0x09加上0xA0得到0xA9
- 最后將得到的這兩個十六進制數合並,得到一個字符的GB2312碼:0xD90xA9
計算機怎么知道正在處理的這個8位是ASCII碼還是GB2312碼?正是通過判定這個字符的大小,如果小於127就代表是ASCII碼,如果碰到連續兩個大於127的8位,那就代表這兩個組成成一個GB2312碼(一個字符的碼位轉換為十六進制后需要分別加上0xA0,是希望讓它的高8位和低8位都大於127)
GB2312的高位和低位都大於127,一共6763個漢字。但后來發現中國的漢字實在太多了,還有很多漢字並不在GB2312字符集里面,於是對GB2312字符集進行擴充,並且不再規定它的低位一定要大於127,它可以小於127,但需要保證高位是大於127的,同時規定計算機只要碰到一個大於127的字節,就表示一個漢字的開始。通過這種方式新增了近2萬個漢字和符號,將這樣的字符集稱作GBK字符集
后來很多少數民族也使用計算機,於是在GBK字符集的基礎上,又新增了幾千個少數民族的字符,對應的字符集就稱作GB18030字符集,對應的編碼就稱作GB18030碼
GB2312 | GBK | GB18030 |
---|---|---|
高位和低位都大於127 | 不再規定低位大於127(在GB2312基礎上擴展) | 在GBK基礎上擴展 |
6763個漢字 | 新增近2萬個漢字和符號 | 新增幾千少數名族字符 |
Unicode
那既然中國可以通過這樣的方式實現字符編碼,世界上那么多國家,每個國家都可以設計出一套屬於自己國家的編碼。世界上有很多編碼,不同國家在進行信息交流的時候,就會發現存在亂碼的現象,這時候ISO提出了一個Unicode標准,旨在收集全球所有的字符,並為每個字符分配唯一的字符編號即碼點(Code Point)。在Unicode標准中,碼點采用十六進制書寫,並加上前綴 U+
,例如U+0041就是拉丁字母A的碼點
Unicode的碼點可以按照使用上的頻繁度划分為17個平面(編號為0-16),平面又稱代碼級別(Code plane)。碼點范圍是:0x000000-0x10FFFF
- 基本的多語言平面(平面0,英文簡寫BMP):收集了使用最廣泛的字符;碼點從U+0000到U+FFFF,共有65536個字符,也就是2字節
- 輔助平面(平面1-16,英文簡寫SMP):包括增補多語言平面(平面1)、增補象形平面(平面2)、保留平面(平面3-13)、增補專用平面等,每個增補平面有65536個碼點;碼點范圍是:0x010000-0x10FFFF
Unicode字符集可以有不同的編碼方式,如UTF-8,UTF-16,UTF-32,這里UTF指的是Unicode Transformation Format,即Unicode轉換格式,即將Unicode編碼空間中每個字符對應的碼點,與字節順序進行一一映射
USC-2、USC-4字符集
一開始Unicode使用的是UCS-2字符集,這個字符和ASCII碼很像,它將所有用到的字符羅列在一起,並且按順序給它標上對應的碼位,然后它的存儲方式也是直接將碼位轉換成對應的二進制信息進行存儲即可。USC-2字符集一共可以表示2的16次方,也就是65536個字符
但是到后來發現這65536個字符還是無法表示世界上所有的字符,於是后面出現了UCS-4字符集。UCS-4字符集是用32位(4個字節)來表示一個字符,一共可以表示2的32次方,將近43億個字符。這基本上就能夠涵蓋世界上所有的字符了,但這樣的編碼規則並沒有世界各國很好的接受,因為它需要的存儲空間比較大。ASCII編碼本來只需要1個字節,現在變成4個字節,擴大了4倍;GB2312編碼原本需要2個字節,現在需要4個字節,擴大了2倍,所以Unicode標准推出來以后很長時間並沒有被廣泛接受。直到后面互聯網時代的來臨,各國之間的信息交流愈加頻繁,這時候不得不對編碼進行重新思考,於是出現了UTF-8這樣的編碼規則
UTF-8 編碼
UTF-8是一種變長編碼方式,一般用1-4個字節來編碼一個Unicode字符,是目前應用最廣泛的一種編碼方式
UTF-8編碼方式:第一個字節提示了這個Unicode編碼由幾個字節組成
- 首字節以0開頭,表示單字節編碼,對應的區間是0-7F
- 首字節以110開頭;表示雙字節編碼,后續字節以10開頭,對應的區間是80-7FF
- 首字節以1110開頭,表示三字節編碼,后續字節以10開頭,對應的區間是800-FFFF
- 首字節以11110開頭,表示四字節編碼,后續字節以10開頭,對應的區間是10000-10FFFF
十六進制碼點區間 | 二進制編碼樣式 |
---|---|
0x00000000 ~ 0x0000007F(0-127) | 0xxxxxxx |
0x00000080 ~ 0x000007FF(128-2047) | 110xxxxx 10xxxxxx |
0x00000800 ~ 0x0000FFFF(2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx |
0x00010000 ~ 0x0010FFFF(65536以上) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
舉例說明如何得到UTF-8編碼:
“王”這個字符在UCS-4字符集里面的碼位是 0x0000738B
,將這個碼位轉換為二進制 0000 0000 0000 0000 0111 0011 1000 1011
,然后可以發現“王”這個字的碼位屬於第三區間,因為第三區間是從800到FFFF的。此時得到了“王”這個碼位的二進制形式和它對應區間的編碼樣式,只需將二進制信息從后往前依次插入到編碼樣式中,得到 11100111 10001110 10001011
,最后把它轉換成十六進制,最終的UTF-8編碼就是 0xe7 0x8e 0x8b
UTF-8解碼方式:
- 當讀取到一個字節的首位是0,表示這是一個單字節編碼的ASCII字符
- 當讀取到一個字節的首位是1,表示這是一個多字節編碼的字符;首字符是1之后繼續讀取直到遇到0為止,一共遇到多少個1就表示該字符為幾個字節的編碼
- 當讀到一個字節的首位是1,緊接着讀取到一個0,則該字節就是多字節編碼的后續字節
舉例說明UTF-8編碼方式:
中文的“黃”字,Unicode碼點為 U+9EC4
,轉換為二進制表示則為 1001 1110 1100 0100
,在UTF-8編碼下,這個字符屬於第三區間,應占用3個字節。於是將二進制按順序從低位到高位插入到各個字節的有效位上,得到“黃”的UTF-8編碼為 11101001 10111011 10000100
,轉為16進制則為 0xE9BB84
UTF-8編碼方式的特點
- 優點:變長,節省空間;自同步編碼/自動糾錯性能好,利於傳輸;完全兼容ASCII編碼;無字節序
- 缺點:不利於程序內部處理,如正則表達式檢索
自同步:在傳輸過程中,若存在字節丟失或者存在錯誤的字節序列,也不會影響到其他字節的正常讀取;如讀取了一個10xxxx開頭的字節,但是找不到首字節,就可以將該字節丟棄
UTF-16 編碼
UCS-2將字符碼點直接映射為字符編碼,采用固定的2字節編碼,只覆蓋了BMP的碼點,對於SMP的碼點,2字節的16位二進制數是不足以表示的。而UTF-16擴展了原來的UCS-2,解決了輔助平面碼點的字符無法表示的問題。
它的編碼規則很簡單:BMP中的字符需要一個字節,其他的SMP中需要兩個字節
- BMP中的有效碼點,用固定2字節16位編碼,數值等於對應的碼點,和UCS-2一樣
- 輔助平面中的有效碼點,使用代理進行編碼。在BMP中有一個范圍的碼點是未定義的,被稱為代理區,其碼點范圍是
0xD800~0xDFFF
,共211個碼點。代理區又被分為高代理碼點和低代理碼點。其中高代理碼點范圍是0xD800~0xDBFF
,低代理碼點范圍是0xDC00~0xDFFF
。高代理碼點和低代理碼點結合在一起,就表示一個輔助平面中的字符。由於SMP中的字符共有 220 個,高代理碼點和低代理碼點皆有 210 個取值,二者結合,恰好有 220 種不同的組合,可以表示完Unicode中的字符
因此,當我們遇到兩個字節,發現它的碼點在U+D800到U+DBFF之間,就可以斷定緊跟在后面的兩個字節的碼點,應該在U+DC00到U+DFFF之間,這四個字節必須放在一起解讀
UTF-16編碼舉例說明:
漢字“?”的Unicode碼點為 0x20BB7
,該碼點顯然超出了基本平面的范圍(0x0000-0xFFFF),因此需要使用四個字節表示。首先用 0x20BB7-0x10000
計算超出的部分,然后將其用20個二進制位表示(不足前面補0),結果為 0001000010 1110110111
。接着,將前10位映射到 U+D800 到 U+DBFF之間,后10位映射到 U+DC00 到 U+DFFF即可。U+D800對應的二進制數為 1101100000000000
,直接填充后面的10個二進制位即可,得到 1101100001000010
,轉換成16進制數則為 0xD842
。同理可得,低位為 0xDFB7
。因此得出漢字"?"的UTF-16編碼為 0xD842 0xDFB7
UTF-32 編碼
UTF-32固定以4字節來編碼,ISO 10646中稱UTF-32是UCS-4的一個子集
若用UTF-32來編碼,程序處理會比較簡單,但是所有字符皆占4個字節,比較浪費空間
名詞解釋
- 碼點(Code Point):是指與一個編碼表中的某個字符對應的代碼值
- 代碼單元(Code Unit):指已編碼的文本中,最短的比特組合單元。對UTF-8來說,代碼單元是8比特;對UTF-16來說是16比特;對UTF-32來說是32比特。即UTF-8以1個字節為最小單位,UTF-16以2個字節為最小單位
- 字節序(Byte Order Mark,BOM):出現在文件頭部,表示字節順序。在USC編碼中,有一個叫"ZERO WIDTH NO-BREAK SPACE"字符,其碼點為
0xFEFF
.UCS規范建議,在傳輸字節流時,線傳輸這個字符。這樣,如果接收者收到FEFF
,表明字節流是 Big-Endian(大端字節序),如果接收者收到FFFE
,表明字節流是 Little-Endian(小端字節序)。對UTF-8來說,不需要BOM字節序,但是可以用BOM來表明是UTF-8編碼。0xFEFF
的UTF-8編碼為EF BB BF
,若接收者收到EF BB BF
開頭的字節流,就知道這是UTF-8的字節流了 - 大端序/小端序:大端序就是將高位字節放到低地址處;小端序就是將低位字節放到高地址處。如果不分大小端的話,那么就會出現解讀錯誤,比如我們一次要處理四個字節 12 34 56 78,這四個字節是表示 0x12 34 56 78 還是表示 0x78 56 34 12?不同的解釋最終表示的值不一樣
三種編碼方式比較
編碼方式 | UTF-8 | UTF-16 | UTF-32 |
---|---|---|---|
編碼字節數 | 變長;1-4字節;代碼單元為8位(1字節) | 2字節或4字節;代碼單元為16位(2字節) | 4字節;代碼單元為32位(4字節) |
優點 | 兼容ASCII碼;節省空間;糾錯能力強,利於網絡傳輸 | 最早的編碼方式,適合內存中的Unicode處理,很多語言中作為String類的編碼方式 | 固定字節編碼;簡單,利於程序處理;Unicode碼點和編碼一一對應 |
缺點 | 變長方式不利於程序內部處理 | 不兼容ASCII,增補平面使用代理對,較為復雜,擴展性差 | 不兼容ASCII,浪費存儲空間和網絡帶寬,擴展性差 |
BOM字節序 | 無字節序(可用BOM來表示UTF-8編碼) | 有字節序(UTF-16LE小端序FFFE,UTF-16BE大端序FEFF) | 有字節序(UTF-32LE小端序FFFE,UTF-32BE大端序FEFF) |
具體語言中的使用
Java中的char類型,內部編碼采用的utf-16(歷史原因),以定長方式,2個字節存儲
對於BMP中的字符,一個char就能表示;對於SMP內的字符,一個char可不夠用,需要由2個char來存儲,或者用String來表示