深入理解Emoji(一) —— 字符集,字符集編碼

講故事的小黃瓜發表於2018-11-30

最近在開發中遇到了點Emoji相關的問題,便去了解了一下Emoji的編碼規則,發現其中涉及了許多字符集與字符集編碼的知識點,便趁這個機會做一次這方面的總結梳理。本篇內容主要是對字符集和字符集編碼的知識整理。

1. 字符集與字符集編碼

我們知道,計算機中的所有資訊最終都是以二進位制的形式儲存,所以人機互動中其實伴隨著二進位制的轉換,將我們輸入到計算機的字元(資訊)轉換成計算機能識別的二進位制資料,或將二進位制資料輸出為我們人能識別的字元。那什麼是字元呢?在計算機領域,我們把諸如文字、標點符號、圖形符號、數字等統稱為字元。而由字元組成的集合則成為字符集,字符集由於包含字元的多少與異同而形成了各種不同的字符集。所以,為了讓計算機能識別出我們字符集裡的字元,就需要制定一套規則,這套規則,就是字符集編碼。我們規定字元編碼必須完成如下兩件事:

  1. 規定一個字符集中的字元由多少個位元組表示。
  2. 制定該字符集的字元編碼表,即該字符集中每個字元對應的(二進位制)值。

2. ASCII 碼

上個世紀 60 年代,美國製定了一套字元編碼標準,對英語字元與二進位制位之間的關係,做了統一規定。這被稱為 ASCII 碼,一直沿用至今。

ASCII(American Standard Code for Information Interchange),是一種字元編碼標準,它的字符集為英文字符集,它規定字符集中的每個字元均由一個位元組表示,指定了字元表編碼表,稱為 ASCII 碼錶。它已被國際標準化組織定義為國際標準,稱為 ISO646 標準。

ASCII 碼一共規定了 128 個字元的編碼,比如空格“SPACE”是 32(二進位制00100000),大寫的字母 A是 65(二進位制01000001)等。這 128 個符號(包括32個不能列印出來的控制符號),只佔用了一個位元組的後面 7 位,最前面的 1 位統一規定為 0。這種採用一個位元組來編碼 128 個字元的 ASCII 碼稱為標準 ASCII 碼或者基礎 ASCII 碼。

但是,由於標準 ASCII 字符集字元數目有限,在實際應用中往往無法滿足要求。為此,國際標準化組織又制定了 ISO 2022 標準,它規定了在保持與 ISO646 相容的前提下將 ASCII 字符集擴充為 8 位程式碼的統一方法。 ISO 陸續制定了一批適用於不同地區的擴充 ASCII 字符集,每種擴充 ASCII 字符集分別可以擴充 128 個字元,這些擴充字元的編碼均為高位為 1 的 8 位程式碼(即十進位制數 128~255 ),稱為擴充套件 ASCII 碼。

但是需要注意,各種擴充套件 ASCII 碼除了編碼為 0~127 的字元外,編碼為 128~255 的字元並不相同。比如,130 在法語編碼中代表了 é,在希伯來語編碼中卻代表了字母 Gimel (?),在俄語編碼中又會代表另一個符號。因此,ASCII 碼的問題在於儘管所有人都在 0 - 127 號字元上達成了一致,但對於 128 - 255 號字元上卻有很多種不同的解釋。與此同時,亞洲語言有更多的字元需要被儲存,一個位元組已經不夠用了。於是,人們開始使用兩個位元組來儲存字元。各種各樣的編碼方式成了系統開發者的噩夢,因為他們想把軟體賣到國外。於是,他們提出了一個“內碼錶”的概念,可以切換到相應語言的一個內碼錶,這樣才能顯示相應語言的字母。在這種情況下,如果使用多語種,那麼就需要頻繁的在內碼錶內進行切換。

3. Unicode

所以,人們最終意識到需要一種標準的規範來展示世界上的所有字元,於是,Unicode就應運而生了。Unicode最初代表著一個字符集,後來慢慢演化成為廣義的一個標準,定義了一個字符集以及一系列的編碼規則,即 Unicode 字符集和 UTF-8、UTF-16、UTF-32 等等編碼…

3.1 碼點

一個字符集一般可以用一張或多張由多個行和多個列所構成的二維表來表示。

二維表中行與列相交的點,稱之為碼點(Code Point程式碼點),也稱之為碼位(Code position程式碼位);每個碼點分配一個唯一的編號,稱之為碼點值或碼點編號,除開某些特殊區域(比如代理區、專用區)的非字元碼點和保留碼點,每個碼點唯一對應於一個字元。通俗的說,碼點就是字元在Unicode中所對應的二進位制數

碼點值最初用兩個位元組的十六進位制數字表示,比如字母A的Unicode碼點值為0041,常寫作U+0041,這種形式稱為Unicode碼點名稱。後來隨著Unicode字符集的不斷增補擴大(比如現在的Unicode字符集至少需要21位才能全部表示),碼點值也擴充套件為用三個位元組或以上的十六進位制數字表示。

3.2 碼元

在計算機儲存和網路傳輸時,碼點被對映到一個或多個碼元(Code Unit)。碼元可理解為字元編碼方式CEF(Character Encoding Form)對碼點值進行編碼處理時作為一個整體來看待的最小基本單元(基本單位)。

3.3 平面

Unicode的編碼空間從U+0000到+10FFFF,共有1,112,064個碼點,可用來對映字元. 整個編碼空間可以劃分為17個平面(plane),每個平面包含216(65,536)個碼位。17個平面的碼位可表示為從U+xx0000到U+xxFFFF,其中xx表示十六進位制值從00到10,共計17個平面。第一個平面稱為基本多語言平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0)。其他平面稱為輔助平面(Supplementary Planes)。基本多語言平面內,從U+D800到U+DFFF之間的碼位區段是永久保留不對映到Unicode字元,稱為代理碼點(Surrogate Code Point)。UTF-16就利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字元的碼位進行編碼。除代理碼點之外的稱為Unicode標量值 (Scalar Value)

3.4 編碼方式

一個字元的Unicode碼點是確定的。但是在實際傳輸過程中,由於不同系統平臺的設計不一定一致,以及出於節省空間的目的,對Unicode的編碼方式有所不同。

Unicode的編碼方式稱為Unicode轉換格式(Unicode Transformation Format,簡稱為UTF)。以漢字“漢”為例,它的 Unicode 碼點是 0x6c49,對應的二進位制數是 110110001001001,二進位制數有 15 位,這也就說明了它至少需要 2 個位元組來表示。可以想象,在 Unicode 字典中往後的字元可能就需要 3 個位元組或者 4 個位元組,甚至更多位元組來表示了。這就導致了一些問題,計算機怎麼知道你這個 2 個位元組表示的是一個字元,而不是分別表示兩個字元呢?這裡我們可能會想到,那就取個最大的,假如 Unicode 中最大的字元用 4 位元組就可以表示了,那麼我們就將所有的字元都用 4 個位元組來表示,不夠的就往前面補 0。這樣確實可以解決編碼問題,但是卻造成了空間的極大浪費,如果是一個英文文件,那檔案大小就大出了 3 倍,這顯然是無法接受的。於是,為了較好的解決 Unicode 的編碼問題, UTF-8UTF-16 兩種當前比較流行的編碼方式誕生了。當然還有一個 UTF-32 的編碼方式,也就是上述那種定長編碼,字元統一使用 4 個位元組,雖然看似方便,但是卻不如另外兩種編碼方式使用廣泛。

  • UTF-8 UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字元編碼,是目前網際網路上使用最廣泛的一種 Unicode 編碼方式,它的最大特點就是可變長。它可以使用 1 - 4 個位元組表示一個字元,根據字元的不同變換長度。編碼規則如下:
    1. 對於單個位元組的字元,第一位設為 0,後面的 7 位對應這個字元的 Unicode 碼點。因此,對於英文中的 0 - 127 號字元,與 ASCII 碼完全相同。這意味著 ASCII 碼那個年代的文件用 UTF-8 編碼開啟完全沒有問題。
    2. 對於需要使用 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

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

解碼的過程也十分簡單:如果一個位元組的第一位是 0 ,則說明這個位元組對應一個字元;如果一個位元組的第一位1,那麼連續有多少個 1,就表示該字元佔用多少個位元組。當中間出現不符合規則的位元組編碼,比如11100110 10110001 00011101,則系統會判定前面的 11100110 10110001是無效位元組,從而捨棄掉,直接解析00001001,於是,我們在介面上會看到這一段會出現一個亂碼,但後續是正常的.所以UTF-8是一種十分安全有效的編碼方式

  • UTF-16 上面我們說過碼點和平面的概念,其中有提到UTF-16是利用代理碼點來編碼的一種可變長編碼方式。UTF-16 編碼介於 UTF-32 與 UTF-8 之間,同時結合了定長和變長兩種編碼方法的特點,字元處理方便且速度快,所以許多程式語言的內部編碼都是UTF-16。比如java、js、c#、python。它的編碼規則很簡單:基本平面的字元佔用 2 個位元組(U+0000U+FFFF),編碼後的值與碼點一致。輔助平面的字元佔用 4 個位元組(U+010000U+10FFFF),編碼後碼點被對映成兩個代理碼點。系統可以通過是否是代理碼點來判斷該字元是用2個或者4個位元組表示。 輔助平面的碼點編碼規則:

    1. 碼點減去0x10000,得到的值的範圍為長度20位的0..0xFFFFF
    2. 高位的10位的值(值的範圍為0..0x3FF)被加上0xD800得到第一個碼元或稱作高位代理碼元(High-Surrogate Code Point),範圍是0xD800 ~ 0xDBFF
    3. 低位的10位的值(值的範圍也是0..0x3FF)被加上0xDC00得到第二個碼元或稱作低代理編碼單元(Low-Surrogate Code Unit),範圍是0xDC00..0xDFFF.

    舉個例子,我們日常中使用的笑臉Emoji表情“?”的碼點為U+1F603,是一個輔助平面上的碼點,則減去0x10000得到0xF603,20位二進位制數為0000 1111 0110 0000 0011,高位00 0011 1101十六進位制數為0x3D,低位0 0000 0011十六進位制數為0x03,分別加上0xD8000xDC00得到0xD83D 0XDC03,這就是這個表情的UTF-16編碼。

    UTF-16編碼舉例

    這裡還會涉及到一個位元組序的問題,我會在下篇中詳細講解,這裡不做贅敘。

  • UTF-32 UTF-32 (或 UCS-4)是一種將Unicode字元編碼的協定,對每一個Unicode碼位使用恰好32位元。因為UTF-32對每個字元都使用4位元組,就空間而言,是非常沒有效率的。特別地,輔助平面的字元在大部分檔案中通常很罕見,以致於它們通常被認為不存在佔用空間大小的討論,使得UTF-32通常會是其它編碼的二到四倍。雖然每一個碼位使用固定長定的位元組看似方便,它並不如其它Unicode編碼使用得廣泛。

相關文章