1. 程式人生 > >淺談Unicode編碼

淺談Unicode編碼

如何 實現 應該 英語 代碼 結合 bmp 其他 想象

概述

對於ASCII編碼,相信同學們都比較了解,那麽對於Unicode、UTF-8和UTF-16,它們是怎麽編碼的呢?以及它們之間的關系是什麽呢?它們與ASCII之間又有什麽關系?

本文就來回答這兩個問題。

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

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

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

  • UTF-8: 根據編號大小自動調整占用字節大小,編號小的使用的字節就少,編號大的使用的字節就大。使用的字節個數從1到4個不等。不分區大小端
  • UTF-16: 編號在U+0000到U+FFFF的字符使用2字節存儲,編號在U+10000到U+10FFFF之間的字符使用4字節存儲,區分大小端
  • UTF-32: 使用4字節定長存儲,區分大小端

關於bom

BOM(byte-order mark)文件編碼頭,即字節順序標記。它是插入到以UTF-8、UTF16或UTF-32編碼文件開頭的特殊標記,用來標記多字節編碼文件的編碼類型和字節順序(big-endian或little- endian)。一般用來識別文件的編碼類型。

根據字節序的不同,UTF-16可以被實現為UTF-16LE或UTF-16BE,UTF-32可以被實現為UTF-32LE或UTF-32BE。

BOM編碼頭 常見形式如下:
EF BB BF = UTF-8 (可選標記,因為Unicode標準未有建議)

FE FF = UTF-16, big-endian (大尾字節序標記)
FF FE = UTF-16, little-endian (小尾字節序標記) (也是windows中的Unicode編碼默認標記)

00 00 FE FF = UTF-32, big-endian (大尾字節序標記)
FF FE 00 00 = UTF-32, little-endian (小尾字節序標記)

對於UTF-8來說,BOM標記的有無並不是必須的,是可選的,因為UTF8字節沒有順序,不需要標記。也就是說一個UTF-8文件可能有BOM,也可能沒有BOM。

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 碼點是 0x6c49(110 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 個(2^16)字符,稱為一個平面(plane)。目前,一共有 17 個(2^5)平面,也就是說,整個 Unicode 字符集的大小現在是 2^21。

最前面的 65536 個字符位,稱為基本平面(簡稱 BMP ),它的碼點範圍是從 0 到 2^16-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+D800 到 U+DFFF 是一個空段,即這些碼點不對應任何字符。因此,這個空段可以用來映射輔助平面的字符。

輔助平面的字符位共有 2^20 個,因此表示這些字符至少需要 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。

淺談Unicode編碼