前言
- 同樣來自是搬家的一部分,恐怕近期幾篇文章都還是來自搬移;參見除舊 + 佈新 + 回顧,為什麼 blog 又得再搬一次家? 。
- 本篇節錄、翻譯以及補充說明自 UTF-8: Bits, Bytes, and Benefits;也有調整篇幅與排版。
說明
前情提要
所有電腦儲存的資料基本上就是一堆數字,而文字 (字串) 也是由一堆數字表達而成的。至於要如何定義某些數字代表某些文字,這個過程就稱為編碼。
最直接暴力的方式,就是 Unicode,直接讓把世界上幾乎所有語言用到的「字母」、「文字」都直接對應於一個數字,參見這裡。
不過數字有短有長,在資料傳輸的時候,我們自然希望把常用的內容用比較短的數字表示,這樣比較經濟,這個邏輯應該很直覺。至於「到底如何」分類常用與否、或是完成這個編碼,就又是另一個問題。
UTF-8
是其中一個 Unicode 的編碼方式,他把整數 (0
~ 10FFFF
,16 進位表示) 轉成 byte stream (0101…);且它實際上比大家想的單純。
不囉唆先上表
尷尬,方格子這邊 code block 不夠寬......如果有更好的方法請告訴我。
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
拆開成左右兩部分顯示,要請大家發揮一點想像力假裝他們是同一個表格:
Unicode code points
00~7F ( 7 bits)
0080~07FF (11 bits)
0800~FFFF (16 bits)
010000~10FFFF (21 bits)
UTF-8 encoding (binary)
0zzzzzzz
110yyyyy 10zzzzzz
1110xxxx 10yyyyyy 10zzzzzz
11110www 10xxxxxx 10yyyyyy 10zzzzzz
不難看出,總共分成 4 組數字範圍。
第一組 00
~7F
- 十六進位下的
7F
是 127,所以 0 ~ 127 總共對應了128
個數字。 - 二進位下,0 ~ 127 會需要 7 個 bits 儲存,這 7 bits 也就對應了表右邊的 7 個 z
zzzzzzz
- 例如
6A
(106) 這個數字,轉成二進位是1101010
填入後得到01101010
跟 ASCII 完全一樣。這是特意兼容 ASCII 編碼的結果。
第二組:0080
~ 07FF
共 1920
個數字,用來表示各國拼音字母。
維基百科:… 帶有附加符號的拉丁文、希臘文、西里爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母則需要兩個位元組編碼 (Unicode 範圍由U+0080
至U+07FF
)
0080
~07FF
,數字數量是07FF - 007F = 0780
(十進位1920
);1920
用二進位表示,最少需要11
bits (2
的11
次方是2048
),把這11
bits 前 5 碼填入表右邊的yyyyy
、後 6 碼填入右邊的zzzzzz
即代表 encoding 的結果。- 例如
026A
這個數字 (十進位618
),轉成11
bit 是01001101010
,則 encoding 的結果是11001001 10101010
,像是這樣:
上表第二組:
0080~07FF (11 bits) 110yyyyy 10zzzzzz
026A 16 進位 = 01001101010
分成前五、後六 = 01001 101010
yyyyy zzzzzz
-> 11001001 10101010
第三組:0800
~FFFF
仿照一樣的算法會得到 63488
(十進位),不過實際上會用到的只有 61440
個
FFFF - 07FF = F800
實際上共63488
個數字。不過在編碼上,實際會用到的只有000800
~00D7FF
00E000
~00FFFF
這兩段,因此實際會用到的數字數量是D000 + 2000 = F000
(十進位 61440) 個數字。
61440
用二進位表示,最少需要16
bits (2
的16
次方是65536
),前四碼填入表右邊的xxxx
、中間 6 碼填入yyyyyy
、最後 6 碼填入zzzzzz
即代表 encoding 的結果。- 例如
E12A
這個數字 (十進位57642
),換成二進位是16
bit :1110000100101010
,因此 encoding 的結果是11101110 10000100 10101010
。
上表第三組:
0800~FFFF (16 bits) 1110xxxx 10yyyyyy 10zzzzzz
E12A 16 進位 = 1110000100101010
分成 4, 6, 6 = 1110 000100 101010
xxxx yyyyyy zzzzzz
-> 11101110 10000100 10101010
(如果這邊可以替文字上色的話會清楚更多;希望方格子可以設計這個 feature。)
我們的中文也落在這個範圍,可以參考 Wiki 中日韓統一表意文字 (Unicode區段)。
例如 中文
U+4E2D U+6587
十進位:
20013 25991
也就是在 unicode 中,中
是第 20013 個字,而 文
是第 25991 個,夠暴力吧。
每次想到世界上曾經有一些人,把幾乎所有中 (日韓) 文字都跟某個數字關聯起來,就覺得頭皮發麻、枯燥到令人髮指,一方面是數量這麼多,一方面是要怎麼決定優先順序呢?
第四組
- 類推,累了就不算了😂
性質
完全兼容 ASCII
所有的 ASCII 檔案就是 UTF-8 檔案,完全兼容。參考上面第一組編碼的說明。
在 UTF-8 編碼中看到 ASCII bytes 可以確保是完全一樣的字。例如在 UTF-8 檔案中看到 0x7A
(01111010
),可以確認他一定代表字母 z
、ASCII bytes 也不會被其他 UTF-8 編碼覆蓋。
理由是只有第一組會是 0
開頭,因此看到 0
就是第一組、也就是 ASCII 字母範圍。
而這個 z
(二進位 01111010
) 也不會在 UTF-8 中被編碼成 11000001 10111010
(第二組的表示法),也就是不會補零變成 00001111010
。因為原則上是要能用最短的編碼表示。
如果編碼失敗,實務上會用 Unicode replacement character (FFFD
) 表示,
長這樣:�
是否似曾相識?沒錯,連失敗都有一個數字代表他;FFFD
代表他是第 65533 個字。
UTF-8 is self-synchronizing:
仔細觀察上表的右手邊,除了開頭第一組之外,其他組的內部、後面跟隨的 bytes 的都是 10______
開頭。例如第三組 1110xxxx
10
yyyyyy
10
zzzzzz
。
這些 bytes 稱作 continuation byte,代表是同一組 encoding 的字元,也就能快速找到下一個字的開頭 —— 下一組 bytes 如果不是 10
______
,即是另一個字的開頭!很巧妙吧。
字串搜尋 (substring search) 只是 bytes string search
從以上性質可知,只要對用一組組 bytes (8-bits) 比對即可完成字串搜尋的任務。
大部分的程式,如果能正常處理 8-bits 檔案,就成正常處理 UTF-8 編碼檔案:
之所以說「大部分」是因為,如果某些程式會「把這些 bytes 拆開視為個別字元」的話就會出錯;不過現在已經很少程式會有這種狀況了。更常見的情況是以換行 \n
或是空白當作字元分隔的符號,這與 ASCII 的編碼是相同的。也因此 Unix Tool 像是 cat
、cp
、diff
、echo
、head
、tail
等指令都能處理 UTF-8 檔案,就像是在處理 ASCII 檔案一樣。
大部分的作業系統也可以直接處理 UTF-8 的檔名,因為主要的區隔資料夾階層的符號是 /
。
另一方面,grep
、sed
、wc
,這類可以接受任意字串當作 input 的工具,就需要一些修改。
UTF-8 如果以 code point 排序也不用修改:
可以參考上面的表,用左邊方式排序與右邊方式相同。
這也代表這些指令:join
、ls
、sort
(沒有其他參數的情況) 不需要對 UTF-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 在檔案的最開頭,不過這沒有必要、也會影響到「沒有考慮到這個標記」的其他程式。
後記
設計這個編碼真的是巧思連連。
想像現在你有一個任務,總共有 6 萬多個數字,要一一把字母跟文字對應起來,同時也要兼容 ASCII、方便搜尋、方便讀取、方便排序。聽起來就不可能。
恭喜你現在也對 UTF-8 多了一些敬意;也恭喜你,看到這裡又變得更宅了一點。
REF
- https://research.swtch.com/utf8
- https://stackoverflow.com/questions/9356169/utf-8-continuation-bytes
- https://zh.wikipedia.org/wiki/UTF-8
- https://en.wikipedia.org/wiki/Byte_order_mark