詳解字元編碼與 Unicode
- 2022 年 9 月 18 日
- 筆記
人類交流使用 A
、B
、C
、中
等字元,但電腦只認識 0
和 1
。因此,就需要將人類的字元,轉換成電腦認識的二進位編碼。這個過程就是字元編碼。
ASCII
最簡單、常用的字元編碼就是 ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼),它將美國人最常用的 26 個英文字元的大小寫和常用的標點符號,編碼成 0
到 127
的數字。例如 A
映射成 65
(0x41
),這樣電腦中就可以用 0100 0001
這組二進位數據,來表示字母 A
了。
ASCII 編碼的字元可以分成兩類:
- 控制字元:
0
–31
和127
(0x00
–0x1F
和0x7F
) - 可顯示字元:
32
–126
(0x20
–0x7E
)
具體字元表可以參考:ASCII – 維基百科,自由的百科全書。
Unicode
ASCII 只編碼了美國常用的 128 個字元。顯然不足以滿足世界上這麼多國家、這麼多語言的字元使用。於是各個國家和地區,就都開始對自己需要的字元設計其他編碼方案。例如,中國有自己的 GB2312,不夠用了之後又擴展了 GBK,還是不夠用,又有了 GB18030。歐洲有一系列的 ISO-8859 編碼。這樣各國人民就都可以在電腦上處理自己的語言文字了。
但每種編碼方案,都只考慮了自己用到的字元,沒辦法跨服交流。如果一篇文檔里,同時使用了多種語言的字元,總不能分別指定哪個字元使用了那種編碼方式。
如果能統一給世界上的所有字元分配編碼,就可以解決跨服交流的問題了,Unicode 就是來干這個事情的。
Unicode 統一編碼了世界上大部分的字元,例如將 A
編碼成 0x00A1
,將 中
編碼成 0x4E2D
,將 α
編碼成 0x03B1
。這樣,中國人、美國人、歐洲人,就可以使用同一種編碼方式交流了。
一個 Unicode 字元可以使用 U+
和 4 到 6 個十六進位數字來表示。例如 U+0041
表示字元 A
、U+4E2D
表示字元 中
,U+03B1
表示字元 α
。
Unicode 最初編碼的範圍是 0x0000
到 0xFFFF
,也就是兩個位元組,最多 65536 (2^16
) 個字元。但隨著編碼的字元越來越多,兩個位元組的編碼空間已經不夠用,因此又引入了 16 個輔助平面,每個輔助平面同樣最多包含 65536 個字元。原來的編碼範圍稱為基本平面,也叫第 0 平面。
各平面的字元範圍和名稱如下表:
平面 | 字元範圍 | 名稱 |
---|---|---|
0 號平面 | U+0000 – U+FFFF |
基本多文種平面 (Basic Multilingual Plane, BMP) |
1 號平面 | U+10000 – U+1FFFF |
多文種補充平面 (Supplementary Multilingual Plane, SMP) |
2 號平面 | U+20000 – U+2FFFF |
表意文字補充平面 (Supplementary Ideographic Plane, SIP) |
3 號平面 | U+30000 – U+3FFFF |
表意文字第三平面 (Tertiary Ideographic Plane, TIP) |
14 號平面 | U+E0000 – U+EFFFF |
特別用途補充平面 |
15 號平面 | U+F0000 – U+FFFFF |
保留作為私人使用區(A 區)(Private Use Area-A, PUA-A) |
16 號平面 | U+100000 – U+10FFFF |
保留作為私人使用區(B 區)(Private Use Area-B, PUA-B) |
每個平面內還會進一步劃分成不同的區段。每個平面和區段具體說明參考 Unicode字元平面映射 – 維基百科,自由的百科全書;漢字相關的區段說明參考 中日韓統一表意文字 – 維基百科,自由的百科全書。Unicode 所有字元按平面和區段查找,可以參考 Roadmaps to Unicode;按區域和語言查找可以參考 Unicode Character Code Charts。
字元編碼的基本概念
「字元編碼」是一個模糊、籠統的概念,為了進一步說明字元編碼的過程,需要將其拆解為一些更加明確的概念:
字元 (Character)
人類使用的字元。例如:
A
;中
等。
編碼字符集 (Coded Character Set, CCS)
把一些字元的集合 (Character Set) 中的每個字元 (Character),映射成一個編號或坐標。例如:
- 在 ASCII 中,把
A
編號為65
(0x41
); - 在 Unicode 中,把
中
編號為0x4E2D
; - 在 GB2312 中,把
中
映射到第 54 區第 0 位。
這個映射的編號或坐標,叫做 Code Point。
Unicode 就是一個 CCS。
字元編碼表 (Character Encoding Form, CEF)
把 Code Point 轉換成特定長度的整型值的序列。這個特定長度的整型值叫做 Code Unit。例如:
- 在 ASCII 中,
0x41
這個 Code Point 會被轉換成0x41
這個 Code Unit; - 在 UTF-8 中,
0x4E2D
這個 Code Point 會被轉換成0xE4 B8 AD
這三個 Code Unit 的序列。
我們常用的 UTF-8、UTF-16 等,就是 CEF。
字元編碼方案 (Character Encoding Scheme, CES)
把 Code Unit 序列轉換成位元組序列(也就是最終編碼後的二進位數據,供電腦使用)。例如 :
UTF-16 BE、UTF-32 LE 等,就是 CES。
這些概念間的關係如下:
因此,我們說 ASCII 是「字元編碼」時,「字元編碼」指的是上面從 Character 到位元組數組的整個過程。因為 ASCII 足夠簡單,中間的 Code Point 到 Code Unit,再到位元組數組,都是一樣的,沒必要拆開說。
而我們說 Unicode 是「字元編碼」時,「字元編碼」其實指的僅是上面的 CCS 部分。
同理,ASCII、Unicode、UTF-8、UTF-16、UTF-16 LE,都可以籠統的叫做「字元編碼」,但每個「字元編碼」表示的含義都是不同的。可能是 CCS、CEF、CES,也可能是整個過程。
Unicode 轉換格式
Unicode 只是把字元映射成了 Code Point (字元編碼表,CCS)。將 Code Point 轉換成 Code Unit 序列(字元編碼表,CEF),再最終將 Code Unit 序列轉換成位元組序列(字元編碼方案,CES),有多種不同的實現方式。這些實現方式叫做 Unicode 轉換格式 (Unicode Transformation Format, UTF)。主要包括:
- UTF-32
- UTF-16
- UTF-8
UTF-32
UTF-32 將每個 Unicode Code Point 轉換成 1 個 32 位長的 Code Unit。
UTF-32 是固定長度的編碼方案,每個 Code Unit 的值就是其 Code Point 的值。例如 0x00 00 00 41
這個 Code Unit,就表示了 0x0041
這個 Code Point。
UTF-32 的一個 Code Unit,需要轉換成 4 個位元組的序列。因此,有大端序 (UTF-32 BE) 和小端序 (UTF-32 LE) 兩種轉換方式。
例如 0x00 00 00 41
這個 Code Unit,使用 UTF-32 BE 最終會編碼為 0x00 00 00 41
;使用 UTF-32 LE 最終會編碼為 0x41 00 00 00
。
UTF-16
UTF-16 將每個 Unicode Code Point 轉換成 1 到 2 個 16 位長的 Code Unit。
對於基本平面的 Code Point(0x0000
到 0xFFFF
),每個 Code Point 轉換成 1 個 Code Unit,Code Unit 的值就是其對應 Code Point 的值。例如 0x0041
這個 Code Unit,就表示了 0x0041
這個 Code Point。
對於輔助平面的 Code Point(0x010000
到 0x10FFFF
),每個 Code Point 轉換成 2 個 Code Unit 的序列。如果還是直接使用 Code Point 數值轉換成 Code Unit,就有可能和基本平面的編碼重疊。例如 U+010041
如果轉換成 0x0001
、0x0041
這兩個 Code Unit,解碼的時候沒辦法知道這是 U+010041
一個字元,還是 U+0001
和 U+0041
兩個字元。
為了讓輔助平面編碼的兩個 Code Unit,都不與基本平面編碼的 Code Unit 重疊,就需要利用基本平面中一個特殊的區段了。基本平面中規定了從 0xD800
到 0xDFFF
之間的區段,是永久保留不映射任何字元的。UTF-16 將輔助平面的 Code Point,編碼成一對在這個範圍內的 Code Unit,叫做代理對。這樣解碼的時候,如果解析到某個 Code Unit 在 0xD800
到 0xDFFF
範圍內,就知道他不是基本平面的 Code Unit,而是要兩個 Code Unit 組合在一起去表示 Code Point。
具體轉換方式是:
- 將輔助平面的 Code Point 的值 (
0x010000
–0x10FFFF
),減去0x010000
,得到0x00000
到0xFFFFF
範圍內的一個數值,也就是最多 20 個比特位的數值 - 將前 10 位的值(範圍在
0x0000
到0x03FF
),加上0xD800
,得到範圍在0xD800
到0xDBFF
的一個值,作為第一個 Code Unit,稱作高位代理或前導代理 - 將後 10 位的值(範圍在
0x0000
到0x03FF
),加上0xDC00
,得到範圍在0xDC00
到0xDFFF
的一個只,作為第二個 Code Unit,稱作低位代理或後尾代理
基本平面中的 0xD800
– 0xDBFF
和 0xDC00
– 0xDFFF
這兩個區段,也分別叫做 UTF-16 高半區 (High-half zone of UTF-16) 和 UTF-16 低半區 (Low-half zone of UTF-16)。
UTF-16 的一個 Code Unit,需要轉換成 2 個位元組的序列。因此,有大端序 (UTF-16 BE) 和小端序 (UTF-16 LE) 兩種轉換方式。
例如 0x0041
這個 Code Unit,使用 UTF-16 BE 最終會編碼為 0x0041
;使用 UTF-16 LE 最終會編碼為 0x4100
。
UTF-8
UTF-8 將每個 Unicode Code Point 轉換成 1 到 4 個 8 位長的 Code Unit。
UTF-8 是不定長的編碼方案,使用前綴來標識 Code Unit 序列的長度。解碼時,根據前綴,就知道該將哪幾個 Code Unit 組合在一起解析成一個 Code Point 了。
具體編碼方式是:
Code Point 範圍 | Code Unit 個數 | 每個 Code Unit 前綴 | 示例 Code Point | 示例 Code Unit 序列 |
---|---|---|---|---|
7 位以內 (0 – 0xEF ) |
1 | 0b0 |
0b0zzz zzzz |
0b0zzz zzzz |
8 到 11 位 (0x80 – 0x07FF ) |
2 | 第一個 0b110 ,剩下的 0b10 |
0b0yyy yyzz zzzz |
0b110y yyyy 10zz zzzz |
12 到 16 位 (0x0800 – 0xFFFF ) |
3 | 第一個 0b1110 ,剩下的 0b10 |
0bxxxx yyyy yyzz zzzz |
0b1110 xxxx 10yy yyyy 10zz zzzz |
17 到 21 位 (0x10000 – 10FFFF ) |
4 | 第一個 0b11110 ,剩下的 0b10 |
0b000w wwxx xxxx yyyy yyzz zzzz |
0b1111 0www 10xx xxxx 10yy yyyy 10zz zzzz |
解碼時,拿到每個 Code Unit 的前綴,就知道這是對應第幾個 Code Unit:
- 前綴是
0b0
,說明這個 Code Point 是一個 Code Unit 組成 - 前綴是
0b110
,說明這個 Code Point 是兩個 Code Unit 組成,後面還會有 1 個0b10
前綴的 Code Unit - 前綴是
0b1110
,說明這個 Code Point 是三個 Code Unit 組成,後面還會有 2 個0b10
前綴的 Code Unit - 前綴是
0b11110
,說明這個 Code Point 是四個 Code Unit 組成,後面還會有 3 個0b10
前綴的 Code Unit
UTF-8 的一個 Code Unit,剛好轉換成 1 個位元組,因此不需要考慮位元組序。
參考上表,對於 ASCII 範圍內的字元,使用 ASCII 和 UTF-8 編碼的結果是一樣的。所以 UTF-8 是 ASCII 的超集,使用 ASCII 編碼的位元組流也可以使用 UTF-8 解碼。
UTF-8 與 UTF-16 對比
Code Point 範圍 | UTF-8 編碼長度 | UTF-16 編碼長度 |
---|---|---|
7 位以內 (0x00 – 0xEF ) |
1 | 2 |
8 到 11 位 (0x0080 – 0x07FF ) |
2 | 2 |
12 到 16 位 (0x0800 – 0xFFFF ) |
3 | 2 |
17 到 21 位 (0x10000 – 10FFFF ) |
4 | 4 |
可以看出只有在 0x00
到 0xEF
範圍的字元,UTF-8 編碼比 UTF-16 短;而在 0x0800
– 0xFFFF
範圍內,UTF-8 編碼是比 UTF-16 長的。
而中文主要在 0x4E00
到 0x9FFF
,如果寫一篇文檔,全都是中文,一個英文字母和符號都沒有。那使用 UTF-8 編碼,可能比 UTF-16 編碼還要多佔用一半的空間。
相關文章: