詳解字元編碼與 Unicode

  • 2022 年 9 月 18 日
  • 筆記

人類交流使用 ABC 等字元,但電腦只認識 01。因此,就需要將人類的字元,轉換成電腦認識的二進位編碼。這個過程就是字元編碼。

ASCII

最簡單、常用的字元編碼就是 ASCII(American Standard Code for Information Interchange,美國資訊交換標準程式碼),它將美國人最常用的 26 個英文字元的大小寫和常用的標點符號,編碼成 0127 的數字。例如 A 映射成 65 (0x41),這樣電腦中就可以用 0100 0001 這組二進位數據,來表示字母 A 了。

ASCII 編碼的字元可以分成兩類:

  • 控制字元:031127 (0x000x1F0x7F)
  • 可顯示字元:32126 (0x200x7E)

具體字元表可以參考:ASCII – 維基百科,自由的百科全書

Unicode

ASCII 只編碼了美國常用的 128 個字元。顯然不足以滿足世界上這麼多國家、這麼多語言的字元使用。於是各個國家和地區,就都開始對自己需要的字元設計其他編碼方案。例如,中國有自己的 GB2312,不夠用了之後又擴展了 GBK,還是不夠用,又有了 GB18030。歐洲有一系列的 ISO-8859 編碼。這樣各國人民就都可以在電腦上處理自己的語言文字了。

但每種編碼方案,都只考慮了自己用到的字元,沒辦法跨服交流。如果一篇文檔里,同時使用了多種語言的字元,總不能分別指定哪個字元使用了那種編碼方式。

如果能統一給世界上的所有字元分配編碼,就可以解決跨服交流的問題了,Unicode 就是來干這個事情的。

Unicode 統一編碼了世界上大部分的字元,例如將 A 編碼成 0x00A1,將 編碼成 0x4E2D,將 α 編碼成 0x03B1。這樣,中國人、美國人、歐洲人,就可以使用同一種編碼方式交流了。

一個 Unicode 字元可以使用 U+ 和 4 到 6 個十六進位數字來表示。例如 U+0041 表示字元 AU+4E2D 表示字元 U+03B1 表示字元 α

Unicode 最初編碼的範圍是 0x00000xFFFF,也就是兩個位元組,最多 65536 (2^16) 個字元。但隨著編碼的字元越來越多,兩個位元組的編碼空間已經不夠用,因此又引入了 16 個輔助平面,每個輔助平面同樣最多包含 65536 個字元。原來的編碼範圍稱為基本平面,也叫第 0 平面。

各平面的字元範圍和名稱如下表:

平面 字元範圍 名稱
0 號平面 U+0000U+FFFF 基本多文種平面 (Basic Multilingual Plane, BMP)
1 號平面 U+10000U+1FFFF 多文種補充平面 (Supplementary Multilingual Plane, SMP)
2 號平面 U+20000U+2FFFF 表意文字補充平面 (Supplementary Ideographic Plane, SIP)
3 號平面 U+30000U+3FFFF 表意文字第三平面 (Tertiary Ideographic Plane, TIP)
14 號平面 U+E0000U+EFFFF 特別用途補充平面
15 號平面 U+F0000U+FFFFF 保留作為私人使用區(A 區)(Private Use Area-A, PUA-A)
16 號平面 U+100000U+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 序列轉換成位元組序列(也就是最終編碼後的二進位數據,供電腦使用)。例如 :

  • 0x0041 這個 Code Unit,使用大端序會轉換成 0x00 41 兩個位元組;
  • 使用小端序會轉換成 0x41 00 兩個位元組。

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(0x00000xFFFF),每個 Code Point 轉換成 1 個 Code Unit,Code Unit 的值就是其對應 Code Point 的值。例如 0x0041 這個 Code Unit,就表示了 0x0041 這個 Code Point。

對於輔助平面的 Code Point(0x0100000x10FFFF),每個 Code Point 轉換成 2 個 Code Unit 的序列。如果還是直接使用 Code Point 數值轉換成 Code Unit,就有可能和基本平面的編碼重疊。例如 U+010041 如果轉換成 0x00010x0041 這兩個 Code Unit,解碼的時候沒辦法知道這是 U+010041 一個字元,還是 U+0001U+0041 兩個字元。

為了讓輔助平面編碼的兩個 Code Unit,都不與基本平面編碼的 Code Unit 重疊,就需要利用基本平面中一個特殊的區段了。基本平面中規定了從 0xD8000xDFFF 之間的區段,是永久保留不映射任何字元的。UTF-16 將輔助平面的 Code Point,編碼成一對在這個範圍內的 Code Unit,叫做代理對。這樣解碼的時候,如果解析到某個 Code Unit 在 0xD8000xDFFF 範圍內,就知道他不是基本平面的 Code Unit,而是要兩個 Code Unit 組合在一起去表示 Code Point。

具體轉換方式是:

  1. 將輔助平面的 Code Point 的值 (0x0100000x10FFFF),減去 0x010000,得到 0x000000xFFFFF 範圍內的一個數值,也就是最多 20 個比特位的數值
  2. 將前 10 位的值(範圍在 0x00000x03FF),加上 0xD800,得到範圍在 0xD8000xDBFF 的一個值,作為第一個 Code Unit,稱作高位代理或前導代理
  3. 將後 10 位的值(範圍在 0x00000x03FF),加上 0xDC00,得到範圍在 0xDC000xDFFF 的一個只,作為第二個 Code Unit,稱作低位代理或後尾代理

基本平面中的 0xD8000xDBFF0xDC000xDFFF 這兩個區段,也分別叫做 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 位以內 (00xEF) 1 0b0 0b0zzz zzzz 0b0zzz zzzz
8 到 11 位 (0x800x07FF) 2 第一個 0b110,剩下的 0b10 0b0yyy yyzz zzzz 0b110y yyyy 10zz zzzz
12 到 16 位 (0x08000xFFFF) 3 第一個 0b1110,剩下的 0b10 0bxxxx yyyy yyzz zzzz 0b1110 xxxx 10yy yyyy 10zz zzzz
17 到 21 位 (0x1000010FFFF) 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 位以內 (0x000xEF) 1 2
8 到 11 位 (0x00800x07FF) 2 2
12 到 16 位 (0x08000xFFFF) 3 2
17 到 21 位 (0x1000010FFFF) 4 4

可以看出只有在 0x000xEF 範圍的字元,UTF-8 編碼比 UTF-16 短;而在 0x08000xFFFF 範圍內,UTF-8 編碼是比 UTF-16 長的。

而中文主要在 0x4E000x9FFF,如果寫一篇文檔,全都是中文,一個英文字母和符號都沒有。那使用 UTF-8 編碼,可能比 UTF-16 編碼還要多佔用一半的空間。


相關文章: