1. 程式人生 > >一文弄懂 Unicode 編碼

一文弄懂 Unicode 編碼

原文連結

Unicode 編碼

ASCII碼

在學校學 C 語言的時候,瞭解到一些計算機內部的機制,知道所有的資訊最終都表示為一個二進位制的字串,每一個二進位制位有 0 和 1 兩種狀態,通過不同的排列組合,使用 0 和 1 就可以表示世界上所有的東西,感覺有點中國“太極”的感覺——“太極生兩儀,兩儀生四象,四象生八卦”。

在計算機種中,1 位元組對應 8 位二進位制數,而每位二進位制數有 0、1 兩種狀態,因此 1 位元組可以組合出 256 種狀態。如果這 256 中狀態每一個都對應一個符號,就能通過 1 位元組的資料表示 256 個字元。美國人於是就制定了一套編碼(其實就是個字典),描述英語中的字元和這 8 位二進位制數的對應關係,這被稱為 ASCII 碼。

ASCII 碼一共定義了 128 個字元,例如大寫的字母 A 是 65(這是十進位制數,對應二進位制是0100 0001)。這 128 個字元只使用了 8 位二進位制數中的後面 7 位,最前面的一位統一規定為 0。

歷史問題

英語用 128 個字元來編碼完全是足夠的,但是用來表示其他語言,128 個字元是遠遠不夠的。於是,一些歐洲的國家就決定,將 ASCII 碼中閒置的最高位利用起來,這樣一來就能表示 256 個字元。但是,這裡又有了一個問題,那就是不同的國家的字符集可能不同,就算它們都能用 256 個字元表示全,但是同一個碼點(也就是 8 位二進位制數)表示的字元可能可能不同。例如,144 在阿拉伯人的 ASCII 碼中是گ

,而在俄羅斯的 ASCII 碼中是 ђ

因此,ASCII 碼的問題在於儘管所有人都在 0 - 127 號字元上達成了一致,但對於 128 - 255 號字元上卻有很多種不同的解釋。與此同時,亞洲語言有更多的字元需要被儲存,一個位元組已經不夠用了。於是,人們開始使用兩個位元組來儲存字元。

各種各樣的編碼方式成了系統開發者的噩夢,因為他們想把軟體賣到國外。於是,他們提出了一個“內碼錶”的概念,可以切換到相應語言的一個內碼錶,這樣才能顯示相應語言的字母。在這種情況下,如果使用多語種,那麼就需要頻繁的在內碼錶內進行切換。

Unicode

最終,美國人意識到他們應該提出一種標準方案來展示世界上所有語言中的所有字元,出於這個目的,Unicode誕生了。

Unicode 當然是一本很厚的字典,記錄著世界上所有字元對應的一個數字。具體是怎樣的對應關係,又或者說是如何進行劃分的,就不是我們考慮的問題了,我們只用知道 Unicode 給所有的字元指定了一個數字用來表示該字元。

對於 Unicode 有一些誤解,它僅僅只是一個字符集,規定了符合對應的二進位制程式碼,至於這個二進位制程式碼如何儲存則沒有任何規定。它的想法很簡單,就是為每個字元規定一個
用來表示該字元的數字,僅此而已。

Unicode 編碼方案

之前提到,Unicode 沒有規定字元對應的二進位制碼如何儲存。以漢字“漢”為例,它的 Unicode 碼點是 0x6c49,對應的二進位制數是 110110001001001,二進位制數有 15 位,這也就說明了它至少需要 2 個位元組來表示。可以想象,在 Unicode 字典中往後的字元可能就需要 3 個位元組或者 4 個位元組,甚至更多位元組來表示了。

這就導致了一些問題,計算機怎麼知道你這個 2 個位元組表示的是個字元,而不是分別表示個字元呢?這裡我們可能會想到,那就取個最大的,假如 Unicode 中最大的字元用 4 位元組就可以表示了,那麼我們就將所有的字元都用 4 個位元組來表示,不夠的就往前面補 0。這樣確實可以解決編碼問題,但是卻造成了空間的極大浪費,如果是一個英文文件,那檔案大小就大出了 3 倍,這顯然是無法接受的。

於是,為了較好的解決 Unicode 的編碼問題, UTF-8 和 UTF-16 兩種當前比較流行的編碼方式誕生了。當然還有一個 UTF-32 的編碼方式,也就是上述那種定長編碼,字元統一使用 4 個位元組,雖然看似方便,但是卻不如另外兩種編碼方式使用廣泛。

UTF-8

UTF-8 是一個非常驚豔的編碼方式,漂亮的實現了對 ASCII 碼的向後相容,以保證 Unicode 可以被大眾接受。

UTF-8 是目前網際網路上使用最廣泛的一種 Unicode 編碼方式,它的最大特點就是可變長。它可以使用 1 - 4 個位元組表示一個字元,根據字元的不同變換長度。編碼規則如下:

對於單個位元組的字元,第一位設為 0,後面的 7 位對應這個字元的 Unicode 碼點。因此,對於英文中的 0 - 127 號字元,與 ASCII 碼完全相同。這意味著 ASCII 碼那個年代的文件用 UTF-8 編碼開啟完全沒有問題。
對於需要使用 N 個位元組來表示的字元(N > 1),第一個位元組的前 N 位都設為 1,第 N + 1 位設為0,剩餘的 N - 1 個位元組的前兩位都設位 10,剩下的二進位制位則使用這個字元的 Unicode 碼點來填充。
編碼規則如下:

Unicode 十六進位制碼點範圍 UTF-8 二進位制
0000 0000 - 0000 007F 0xxxxxxx
0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

根據上面編碼規則對照表,進行 UTF-8 編碼和解碼就簡單多了。

下面以漢字“漢”為利,具體說明如何進行 UTF-8 編碼和解碼。

“漢”的 Unicode 碼點是 0x6c49110 1100 0100 1001),通過上面的對照表可以發現,0x0000 6c49 位於第三行的範圍,那麼得出其格式為 1110xxxx 10xxxxxx 10xxxxxx。接著,從“漢”的二進位制數最後一位開始,從後向前依次填充對應格式中的 x,多出的 x 用 0 補上。這樣,就得到了“漢”的 UTF-8 編碼為 11100110 10110001 10001001,轉換成十六進位制就是 0xE6 0xB7 0x89

解碼的過程也十分簡單:如果一個位元組的第一位是 0 ,則說明這個位元組對應一個字元;如果一個位元組的第一位1,那麼連續有多少個 1,就表示該字元佔用多少個位元組。

UTF-16

在瞭解 UTF-16 編碼方式之前,先了解一下另外一個概念——”平面”。

在上面的介紹中,提到了 Unicode 是一本很厚的字典,她將全世界所有的字元定義在一個集合裡。這麼多的字元不是一次性定義的,而是分割槽定義。每個區可以存放 65536 個(216)字元,稱為一個平面(plane)。目前,一共有 17 個(25)平面,也就是說,整個 Unicode 字符集的大小現在是 221

最前面的 65536 個字元位,稱為基本平面(簡稱 BMP ),它的碼點範圍是從 0 到 2161,寫成 16 進位制就是從 U+0000U+FFFF。所有最常見的字元都放在這個平面,這是 Unicode 最先定義和公佈的一個平面。剩下的字元都放在輔助平面(簡稱 SMP ),碼點範圍從 U+010000U+10FFFF

基本瞭解了平面的概念後,再說回到 UTF-16。UTF-16 編碼介於 UTF-32 與 UTF-8 之間,同時結合了定長和變長兩種編碼方法的特點。它的編碼規則很簡單:基本平面的字元佔用 2 個位元組,輔助平面的字元佔用 4 個位元組。也就是說,UTF-16 的編碼長度要麼是 2 個位元組(U+0000 到 U+FFFF),要麼是 4 個位元組(U+010000 到 U+10FFFF)。那麼問題來了,當我們遇到兩個位元組時,到底是把這兩個位元組當作一個字元還是與後面的兩個位元組一起當作一個字元呢?

這裡有一個很巧妙的地方,在基本平面內,從 U+D800U+DFFF 是一個空段,即這些碼點不對應任何字元。因此,這個空段可以用來對映輔助平面的字元。

輔助平面的字元位共有 220 個,因此表示這些字元至少需要 20 個二進位制位。UTF-16 將這 20 個二進位制位分成兩半,前 10 位對映在 U+D800U+DBFF,稱為高位(H),後 10 位對映在 U+DC00U+DFFF,稱為低位(L)。這意味著,一個輔助平面的字元,被拆成兩個基本平面的字元表示。

因此,當我們遇到兩個位元組,發現它的碼點在 U+D800 到 U+DBFF 之間,就可以斷定,緊跟在後面的兩個位元組的碼點,應該在 U+DC00 到 U+DFFF 之間,這四個位元組必須放在一起解讀。

例子:
這裡寫圖片描述
接下來,以漢字”ji”(見上圖)為例,說明 UTF-16 編碼方式是如何工作的。

漢字”ji”的 Unicode 碼點為 0x20BB7,該碼點顯然超出了基本平面的範圍(0x0000 - 0xFFFF),因此需要使用四個位元組表示。首先用 0x20BB7 - 0x10000 計算出超出的部分,然後將其用 20 個二進位制位表示(不足前面補 0 ),結果為0001000010 1110110111。接著,將前 10 位對映到 U+D800U+DBFF 之間,後 10 位對映到 U+DC00U+DFFF 即可。U+D800 對應的二進位制數為 1101100000000000,直接填充後面的 10 個二進位制位即可,得到 1101100001000010,轉成 16 進位制數則為 0xD842。同理可得,低位為 0xDFB7。因此得出漢字”ji”的 UTF-16 編碼為 0xD842 0xDFB7

Unicode3.0 中給出了輔助平面字元的轉換公式:

H = Math.floor((c-0x10000) / 0x400)+0xD800

L = (c - 0x10000) % 0x400 + 0xDC00

根據編碼公式,可以很方便的計算出字元的 UTF-16 編碼。