徹底弄懂 Unicode 編碼

2021-08-29 15:47:17 字數 4963 閱讀 2527

原文:

今天,在學習 node.js 中的 buffer 物件時,注意到它的 alloc 和 from 方法會預設用utf-8編碼,在陣列中每位對應 1 位元組的十六進製制數。想到了之間學習 es6 時關於字串的 unicode 表示法,突然就很想知道 utf-16 是如何進行編碼的,我嘗試將一些漢字轉換成二進位制數,然後簡單的按 2 個位元組一組轉換成十六進製制,發現對於那些碼點較大的漢字,結果並不僅僅是簡單的二進位制轉十六進製制。於是,我開始在網上找資料,決心徹底弄明白 unicode 編碼。

在學校學 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 碼點是 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

0******x

0000 0080 - 0000 07ff

110***xx 10******

0000 0800 - 0000 ffff

1110***x 10****** 10******

0001 0000 - 0010 ffff

11110*** 10****** 10****** 10******

根據上面編碼規則對照表,進行 utf-8 編碼和解碼就簡單多了。下面以漢字「漢」為利,具體說明如何進行 utf-8 編碼和解碼。

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

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

utf-16

在了解 utf-16 編碼方式之前,先了解一下另外乙個概念——"平面"

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

最前面的 65536 個字元位,稱為基本平面(簡稱 bmp ),它的碼點範圍是從 0 到 $2^-1$,寫成 16 進製就是從 u+0000 到 u+ffff。所有最常見的字元都放在這個平面,這是 unicode 最先定義和公布的乙個平面。剩下的字元都放在輔助平面(簡稱 smp ),碼點範圍從 u+010000 到 u+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是乙個空段,即這些碼點不對應任何字元。因此,這個空段可以用來對映輔助平面的字元。

輔助平面的字元位共有 $2^$ 個,因此表示這些字元至少需要 20 個二進位制位。utf-16 將這 20 個二進位制位分成兩半,前 10 位對映在 u+d800 到 u+dbff,稱為高位(h),後 10 位對映在 u+dc00 到 u+dfff,稱為低位(l)。這意味著,乙個輔助平面的字元,被拆成兩個基本平面的字元表示。

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

接下來,以漢字"?"為例,說明 utf-16 編碼方式是如何工作的。

漢字"?"的 unicode 碼點為0x20bb7,該碼點顯然超出了基本平面的範圍(0x0000 - 0xffff),因此需要使用四個位元組表示。首先用0x20bb7 - 0x10000計算出超出的部分,然後將其用 20 個二進位制位表示(不足前面補 0 ),結果為0001000010 1110110111。接著,將前 10 位對映到 u+d800 到 u+dbff 之間,後 10 位對映到 u+dc00 到 u+dfff 即可。u+d800對應的二進位制數為1101100000000000,直接填充後面的 10 個二進位制位即可,得到1101100001000010,轉成 16 進製數則為0xd842。同理可得,低位為0xdfb7。因此得出漢字"?"的 utf-16 編碼為0xd842 0xdfb7

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

h = math.floor((c-0x10000) / 0x400)+0xd800

l = (c - 0x10000) % 0x400 + 0xdc00

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

徹底弄懂session,cookie,token

我在寫之前看了很多篇session,cookie的文章,有的人說先有了cookie,後有了session。也有人說先有session,後有cookie。感覺都沒有講的很清楚,泛泛而談。希望本篇文章對大家有所幫助 注 本文需要讀者有cookie,session,token的相關基礎知識。什麼是無狀態呢...

徹底弄懂Redis set篇

redis中有兩種集合,一種是無序集合,一種是有序集合,他們之間的相同點就是不重複,不同點就是是否有序,我們分別介紹一下。因為set只要保證加入的元素不重複就好,所以他的底層實現也比較簡單,就是乙個value為空的雜湊表,key就是用來儲存加入的元素值的,我們今天重點介紹的就是sort set 有序...

徹底弄懂React Redux元件通訊

為了方便使用,redux的作者封裝了乙個react專用的庫react redux,講解之前,先來了解一下什麼是容器元件和傻瓜元件?react redux把元件分為容器元件和傻瓜元件 ui元件 容器元件,負責和redux store打交道的元件,處於外層。功能 和redux store打交道,讀取st...