UTF-8 天天用,但你知道他到底怎麼編碼的嗎?



前言

  • 先前讀到一篇文章介紹 UTF-8,突然發現時常轉換來轉換去,卻不知道他的編碼規則究竟是什麼。了解一下順便紀錄一篇。

  • 本篇節錄、翻譯自 UTF-8: Bits, Bytes, and Benefits;並加上額外範例。




說明

Code point → Encoding → Hex

UTF-8 是一個 Unicode 的編碼方式,他把整數 (0 ~ 10FFFF,16 進位表示) 轉成 byte stream (0101…);且它實際上比大家想的單純。

參考下表:

       Unicode code points                   UTF-8 encoding (binary)

        00~7F    ( 7 bits)   →                              0zzzzzzz
    0080~07FF    (11 bits)   →                     110yyyyy 10zzzzzz
    0800~FFFF    (16 bits)   →            1110xxxx 10yyyyyy 10zzzzzz
010000~10FFFF    (21 bits)   →   11110www 10xxxxxx 10yyyyyy 10zzzzzz

這張表看起來似乎有點複雜,但其實概念很簡單:

第一組

00 ~ 7F,共 128 個數字,剛好覆蓋 ASCII 編碼;這其實是特意兼容 ASCII 所用的範圍。

其中 7F 兩位數字,16 進位中表示:127,加上 0 共 128 個數字。

將這個數字轉為二進位後,會得到 7 bits,填入表右邊 zzzzzzz,代表的位置即為 encoding 的結果。

例如

  • unicode 編碼 6A (U+006A),轉成 7 bit 二進位是:1101010,所以 encoding 的結果是 01101010;跟 ASCII 完全一樣。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Unicode                    6    A
Binary (7 bits)          110 1010
                ↓        │││ ││││
Encoding mask           0zzz zzzz
Encoding                0110 1010
Hex                        6    A
Result                       0x6A

第二組

0080 ~ 07FF,共 1920 個數字,用來表示各國拼音字母。

維基百科:… 帶有附加符號的拉丁文、希臘文、西里爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母則需要兩個位元組編碼 (Unicode 範圍由 U+0080U+07FF)

0080 ~ 07FF,數字數量以 16 進位計算:07FF - 007F = 0780,也就是 2048 - 128 = 1920 個數字。

1920 用二進位表示

111 1000 0000  (共 11 bits)

代表最少需要 11 bits 才能表示。

接著把這 11 bits 前 5 碼填入表右邊的 yyyyy、後 6 碼填入右邊的 zzzzzz 即代表 encoding 的結果。

例如

  • unicode 編碼 026A (U+026A),轉成 11 bit 是 01001101010,則 encoding 的結果是 11001001 10101010
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Unicode                  0  2    6    A
Binary (11 bits)         0 10 0110 1010
                         └─┴┴─┴┘
                ↓          /
                       ┌─┬┬┬┐
Align to  mask         0 1001   10 1010
                       │ ││││   ││ ││││
Encoding mask       110y yyyy 10zz zzzz
Encoding            1100 1001 1010 1010
Hex                    C    9    A    A
Result                        0xC9 0xAA

第三組

0800~FFFF 仿照一樣的算法會得到 63488 (FFFF - 0800 + 1 = F800),

不過在編碼上,實際會用到的只有 000800 ~ 00D7FF00E000 ~ 00FFFF 兩段,因此實際會用到的數字是

(D7FF - 0800 + 1) = D000
(FFFF - E000 + 1) = 2000

-------------------------
                    D000 
                  + 2000
                  =======
                  = F000

16 進位的 F000 就是 61440,也就是實際會用到的數字是 61440

61440 用二進位表示

1111 0000 0000 0000  (16 bits)

代表最少需要 16 bits

接著,同樣把前 4 碼填入表右邊的 xxxx、中間 6 碼填入 yyyyyy、最後 6 碼填入 zzzzzz 即代表 encoding 的結果。

  • 例如 E12A 這個數字 (U+E12A),換成二進位是 16 bit :1110000100101010,因此 encoding 的結果是 1110 0001 0010 1010
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Unicode                          E    1    2    A
Binary (16 bits)              1110 0001 0010 1010
                              └┴┴┘ └┘  └┴─┴┘
                ↓            /    /    / 
                         ┌┬┬┐   ┌┐ ┌┬┬┐  
Align to  mask           1110   00 0100   10 1010
                         ││││   ││ ││││   ││ ││││
Encoding mask       1110 xxxx 10yy yyyy 10zz zzzz
Encoding            1110 1110 1000 0100 1010 1010
Hex                    D    D    8    4    A    A
Result                           0xDD  0x84  0xAA

第四組

我們的數字越來越長了 😂

第四組的範圍更寬,從 010000 ~ 10FFFF

1
2
3
4
5
    10FFFF
  - 010000
  +      1
  ────────
    100000  (16 進位)

16 進位的 1000001048576,也就是這個區間可以記錄 1048576 (一百多萬) 個字元。

1048576 用二進位表示

100000000000000000000 (21 bits)

代表最少需要 21 bits 才能表示。

參照最上面列的表,第四組是 11110www 10xxxxxx 10yyyyyy 10zzzzzz,填入就是 enconding 的結果

  • 例如 09AB37 這個數字 (U+09AB37),換成二進位是 21 bit :10011010101100110111,因此 encoding 的結果是 11110010 10011010 10101100 10110111
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Unicode                          0    9    A    B    3    7
Binary (21 bits)                 0 1001 1010 1011 0011 0111
                                 └─┴┘└┴─┴┴┴┘ └┴┴┴─┴┘
                ↓             /        /       / 
                          ┌┬┐   ┌┬─┬┬┬┐   ┌┬─┬┬┬┐  
Align to  mask            010   01 1010   10 1100   11 0111    
                          │││   ││ ││││   ││ ││││   ││ ││││
Encoding mask       1111 0www 10xx xxxx 10yy yyyy 10zz zzzz
Encoding            1111 0010 1001 1010 1010 1100 1011 0111 
Hex                    F    2    9    A    A    C    B    7
Result                               0xF2  0x9A  0xAC  0xB7

性質

  1. 所有的 ASCII 檔案就是 UTF-8 檔案,完全兼容。參考上面第一組編碼的說明。

  2. 在 UTF-8 編碼中看到 ASCII bytes 可以確保是完全一樣的字。例如在 UTF-8 檔案中看到 0x7A (01111010),可以確認他一定代表字母 z、ASCII bytes 也不會被其他 UTF-8 編碼覆蓋。

  3. z 二進位 01111010 不會在 UTF-8 中被編碼成 11000001 10111010 (第二組的表示法),也就是不會編成 00001111010。原則上是要能用最短的編碼表示;如果編碼失敗,實務上會用 Unicode replacement character (FFFD) 表示,長這樣:� (似曾相識?)

  4. UTF-8 is self-synchronizing

    • 仔細觀察上表的右手邊,除了開頭第一組之外,其他組的內部、後面跟隨的 bytes 的都是 10______ 開頭。

    • 例如第三組 1110xxxx 10yyyyyy 10zzzzzz

    • 這些 bytes 稱作 continuation byte,代表是同一組 encoding 的字元,也就能快速找到下一個字的開頭 — 下一組 bytes 如果不是 10______ ,即是另一個字的開頭。

  5. 字串搜尋 (substring search) 只是 bytes string search:從以上性質可知,只要對用一組組 bytes (8-bits) 比對即可完成字串搜尋的任務。

  6. 大部分的程式,如果能正常處理 8-bits 檔案,就成正常處理 UTF-8 編碼檔案:

    • 之所以說「大部分」是因為,如果某些程式會把這些 bytes 拆開視為個別字元的話就會出錯;不過現在已經很少程式會有這種狀況了。更常見的情況是以換行 \n 或是空白當作字元分隔的符號,這與 ASCII 的編碼是相同的。也因此 Unix Tool 像是 catcpdiffechoheadtail 等指令都能處理 UTF-8 檔案,就像是在處理 ASCII 檔案一樣。

    • 大部分的作業系統也可以直接處理 UTF-8 的檔名,因為主要的區隔資料夾階層的符號是 /

    • 另一方面,grepsedwc,這類可以接受任意字串當作 input 的工具,就需要一些修改。

  7. UTF-8 如果以 code point 排序也不用修改:

    • 可以參考上面的表,用左邊方式排序與右邊方式相同。

    • 這也代表這些指令:joinlssort (沒有其他參數的情況) 不需要對 UTF-8 編碼額外處理。

  8. UTF-8 沒有 “byte order”:

    • Unicode 定義了 byte order mark (BOM),用來決定 bytes 裡面 bits 讀取的順序;不過這個值不會顯示出任何字元。例如 UTF-16 的 FFFE

    • UTF-8 是 byte encoding. 不區分 little endian 或 big endian。

    • 部分程式可能會寫 UTF-8 編碼後的 BOM 在檔案的最開頭,不過這沒有必要、也會影響到「沒有考慮到這個標記」的其他程式。




REF

Licensed under CC BY-NC-SA 4.0
最後更新 2025-05-15 02:51

主題 StackJimmy 設計