一文讀懂位元組、字符與字符編碼

位元組、字符與字符編碼

位元組

(bit)是計算機中信息的最小單元。位是由電路實現的,硬件底層使用數字電路,以電壓的高低作為記錄信息的方式:較高的電壓表示數值「1」,較低的電壓表示數字「0」。因此,一個位有兩種可能的值,二進制是一種非常適合表示計算機存儲數據的方式。

計算機使用位元組作為最小的存儲單元,大多數計算機上,一個位元組等於 8 個位,因此一個位元組能表示的數字範圍為 0 – 255 ,或二進制形式下的 0b0000 0000 – 0b1111 1111 。如果採用 16 進制表示,那麼可以很方便地表示為 0x00 – 0xFF 。

多個位元組按順序組合,便可以組成任意數據。

字符與編碼

ASCII編碼

位元組只能表示一個數字,為了讓人們可以更好地明白其中的含義,需要將其解讀為文字符號,即字符,然後顯示在屏幕上。例如,如果一個位元組中存儲的數據是 0x08 ,如果計算機將其解讀為第八個英文字母,那麼計算機處理之後可以在屏幕上顯示「h」;如果連續 5 個位元組中存儲的數據是 [0x08, 0x05, 0x0C, 0x0C, 0x0E] ,計算機處理之後就可以根據類似的規則轉換為連續的英文字符「hello」,根據這種方式就可以將計算機中保存的數據轉換為人們容易理解的形式。

根據以上思路,人們制定了 ASCII(American Standard Code for Information Interchange, 美國標準信息交換代碼),使用如下圖所示的數值-字符對應關係表:

ASCII 是一種編碼規則,利用該規則,便可以利用數字替換實際的形狀符號。特別地,數值 0 – 31 表示控制字符,而不是實際可顯示的形狀符號。例如,當計算機處理時遇到數值 0x07 ,它會控制某些設備發出響鈴,而不是控制屏幕顯示符號;當計算機遇到遇到數值 0x10 ,那麼就表示一行的結束,應該控制屏幕從下一行開始顯示字符。

這種編碼規則是雙向的,對字符編碼(encoding)可以將字符串轉換為位元組形式的數據;而對位元組數據解碼(decoding)則可以將位元組形式的數據轉換為字符串,並顯示出來。

ASCII 使用的字符代碼範圍從 0 到 127 ,對應 7 個二進制位,完全可以使用一個位元組存儲一個字符代碼。這種情況下多餘的 1 個二進制位還可以用作校驗,檢查這個字符在存儲的過程中有沒有發生意外變動。

GBK編碼

ASCII 碼可以表示出所有的英文字符、數字和常用標點,但是一個位元組最多只有 256 種不同的值,儘管 ASCII 只使用了其中的一半,但還是不足以容納其它語言的字符。常用的漢字大約有 3000 個,已經遠遠超出了一個位元組的容納能力了,因此這種情況下需要規定一套新的字符編碼規則。

為了能表示更多的字符,許多編碼方案往往用多個位元組來指代一個字符。GB2313 是一種用於表示漢字的編碼方案,它擴展自 ASCII 碼,使用兩個位元組來表示一個漢字。

為了兼容 ASCII 碼,一個數值小於等於 127 的字符的意義與原來相同,但兩個連續的、數值大於 127 的字符連在一起時,就表示一個漢字。但是 GB2313 也沒有完全用盡兩個位元組,前面的一個位元組(稱為高位元組)的範圍從 0xA1 至 0xF7 ,後一個位元組(稱為低位元組)的範圍從 0xA1 到 0xFE ,組合起來可以表達出大約 7000 多個簡體漢字,以及必要的拼音符號、希臘字母、甚至日文假名都包括在內。

為了區分中文的標點和英文的標點,GB2313 編碼為數字、標點、字母也重新編排了兩個位元組長的編碼,也就是輸入法中存在的「全角」字符。而兼容的 ASCII 中存在的標點符號就是「半角」字符。

為了表示更多的字符,GB2313 後來再次被擴展,將兩個位元組中的所有組合全部使用,發展為了 GBK 編碼。GBK 向下兼容 GB2312 的所有編碼情況的同時,增加了許多生僻的漢字和不太常用的符號。

GB2312 和 GBK 是對 ASCII 的中文擴展,不同的國家或地區都針對本國語言產生的新的編碼標準。基於這種用兩個或多個位元組表示一個字符的編碼思路,台灣地區針對繁體推出了 Big5 編碼方案,日本針對日文與日文漢字推出了 Shift_JIS 編碼方案,歐洲地區也有類似的編碼方案。它們都兼容 ASCII 編碼,也就是說如果一個位元組的值小於等於 127 ,那麼就按 ASCII 編碼處理;如果大於 127 ,那麼就和後續的位元組一起當做當前編碼的內容處理。

在 Windows 等系統上也可以看到一種稱為「ANSI」的編碼,實際上這是一種誤稱,實際代表的是系統當前使用的本地化編碼方案。如果改變系統設置里的區域,相應的編碼方案也將改變。

Unicode

Unicode字符集

隨着互聯網的興起,世界各地的信息開始在互聯網上流通。這時,不同的編碼方案就免不了發生碰撞。一串相同的位元組序列使用不同的編碼解碼,得到的信息是完全不一樣的。例如,中文 “你好” 如果使用 GBK 編碼,得到的位元組序列為 [0xc4, 0xe3, 0xba, 0xc3] ,但反過來如果使用 Big5 對這串位元組序列解碼,得到的內容為 “斕疑” ,如果使用 Shift_JIS 解碼,得到的內容為 “ト羲テ” 。這也就導致不同地區發送的位元組形式的消息,如果被另一個地區接收到,那麼使用另一個地區的編碼對其處理後的結果完全沒法閱讀,這就是通常看到的「亂碼」。

因此,ISO(International Organization for Standardization, 國際標誰化組織)重新制定了一套完整的編碼方案,稱為 Universal Coded Character Set ,俗稱 Unicode 或「萬國碼」。Unicode 在設計時,包括了地球上所有文字和和符號,甚至還有多餘的編號保存簡單的表情(emoji)。

Unicode 使用兩個位元組統一表示所有的字符,包括原有的 ASCII 字符也不例外。不過原有的 ASCII 除了由一個位元組擴展成兩個位元組外,還是按原有的順序佔據 U+0000 – U+007F 這最前面的位置。除此之外,其它語言文字也按照一定規則被編排進去,例如漢字佔據其中的 U+4E00 – U+9FA5 這些碼位。

Unicode 從誕生起也一直在發展。最早的 Unicode 使用兩個位元組來表示為一個字符,這種標準稱為 UCS-2(其中 UCS 指 Unicode Character Set),因此總共可以組合出 65535 不同的字符。然而 Unicode 的野心是很大的,它除了表示文字、符號之外,還想表示一個文字的多種不同變體形式。例如,一個漢字有簡體和繁體兩種表示形式,而繁體的漢字在香港、台灣和日本的寫法可能略微有差別的。再加上 Unicode 包含了許多表情符號,因此這 65535 個字符很快就不夠用了。

基於此,Unicode 提供了 UCS-4 方案,使用四個位元組來表示一個字符。UCS-4 是向下兼容 UCS-2 的,其中最高位元組的最高位為 0 ,其它 7 位將 Unicode 字符集劃分為了 128 個(group),每個組再根據次高位元組劃分為 256 個平面(plane),每個平面根據次低位元組劃分為 256 (row),每行有 256 個碼位(cell),這樣就可以組合出約 21 億個不同的字符出來,這下完全夠用了。

UCS-4 不僅是完全夠用,甚至還過於冗餘,這 21 億個碼位不可能用的完,更不可能一下塞滿,只能根據需求向上添加。截止 2021 年 9 月,Unicode 只編排了 144,697 個字符,分佈在平面 0 、平面 1 、平面 2 、平面 14 、平面 15 和平面 16 上。其中平面 15 和平面 16 只作為專用區使用。所謂專用區,就是保留給大家放自定義字符的區域。例如,蘋果公司就在專用區上定義了一系列的公司圖標,但這些圖標在 Android 和 Windows 平台都無法正確顯示。

UTF編碼方案

目前的 Unicode 連平面都只使用了一個零頭,更用不到組了。這種使用 4 個位元組存儲一個字符的形式顯然過於浪費,尤其是對於只需要使用 ASCII 字符的場景來說,足足有 3 倍的空間浪費。在互聯網傳輸數據時,過多冗餘的信息會造成傳輸緩慢、收發效率低下的問題。基於此,Unicode 提出了 UCS Transformation Format ,簡稱 UTF ,旨在對現有的 Unicode 字符集重新再次編碼,再次編碼後的字符可以按照一定規則重新解碼為 Unicode 字符集。

最簡單的編碼方案稱為 UTF-32 ,即直接使用 UCS-4 的四位元組共 32 位的形式。這種形式的編碼壓根沒有減少字符佔用的空間,因此並不實用。Unicode 實際只使用了 32 位中的 21 位,既然 Unicode 並沒有用到組,那麼這部分空間完全可以壓縮。Unicode 也只用了平面的一個零頭,這部分空間如果能進一步被壓縮,那顯然是更好的。

但是再怎麼壓縮,存儲的最小單元是位元組,為了支持到平面這個維度,最小也只能壓縮到 3 位元組,這種情況下對於多數只需要使用 ASCII 字符的場景來說,還是有 2 倍的空間浪費。

UTF-16

基於此,一種變長的編碼方案 UTF-16 被提出:對於 Unicode 碼小於 U+10000 的字符(也就是原先的 UCS-2 字符),使用 2 個位元組直接存儲,不用進行編碼轉換;但對於 U+10000 – U+10FFFF 之間的字符,則需要使用 4 個位元組存儲:首先將字符對應的 Unicode 碼 減去 0x10000 ,得到的結果不超過 20 位,再將這 20 位分為高 10 位和低 10 位,分別塞進前兩個位元組的低 10 位和後兩個位元組的低 10 位中。並且前兩個位元組剩餘的 6 個二進制位固定為 0b110110 ,後兩個位元組剩餘的 6 個二進制位固定為 0b110111 。

下表展示了 UTF-16 對 Unicode 的編碼規則:

Unicode 編碼(十六進制) Unicode 編碼(有效二進制) UTF-16編碼方式
0x0000 0000 – 0x0000 FFFF 0b xxxxxxxx xxxxxxxx 0b xxxxxxxx xxxxxxxx
0x0001 0000 – 0x0010 FFFF 0b yyyy yyyyyyxx xxxxxxxx 0b 110110yy yyyyyyyy 110111xx xxxxxxxx

同時,為了區分兩個位元組的 UTF-16 和四個位元組的 UTF-16 ,Unicode 還專門保留了 U+D800( 0xD8 的二進制為 0b110110 00 )到 U+DFFF( 0xDF 的二進制為 0b110111 11 )不被任何字符使用,這樣只要解析到 0b110110yy 形式的位元組或 0b110111xx 形式的位元組,就可以認為這是一個四位元組的 UTF-16 了。

這種編碼方案十分複雜,但它至少成功壓縮了 Unicode 編碼的存儲。由於目前最多只使用到第 16(即第 0x10 )平面,因此 UTF-16 可以完全對目前所有的 Unicode 字符編碼。

Big Endian與Little Endian

在網絡發送數據時,還有一個問題需要解決:一個字符有多個位元組,但在發送時只能一個位元組一個位元組發送。操作系統可能有兩種發送方式:可以先發送最高位元組,也可以先發送最低位元組。這兩種發送方式會造成數據的解讀不同,從而破壞 Unicode 編碼。

不過 Unicode 定義了一個專門的控制符:零寬度非換行空格(zero width no-break space ,它的 Unicode 碼位為 U+FEFF ,如果先發送高位(這種發送方式稱為 big endian ,大端法),那麼目標會先接收到 0xFE 位元組;反之如果先發送低位(這種發送方式稱為 little endian ,小端法),那麼目標會先接收到 0xFF 位元組。

這樣,只要根據最先接收到的字符,就可以判斷是高位先發送還是低位先發送。這種用來判斷哪個位元組先發送的特殊字符稱為 byte order mark (BOM)。在本地使用 UTF-16 存儲文本文件時,也需要在文件頭帶上 BOM 。

UTF-8

UTF-16 儘管對編碼實現了一次壓縮,但是這種使用二或四位元組的形式還是有一些問題,尤其是 ASCII 碼也被強制編排為兩位元組,這對於大多數使用 ASCII 的情況還是不夠友好。不過,Unicode 還存在一種更強大的編碼方案:UTF-8 。

UTF-8 使用控制位與字符位組成,控制位表示當前字符佔據的位元組數,字符位則對應真正的 Unicode 字符位。其中:

  • 如果一個位元組的最高位為 0 ,那麼該字符就佔據一個位元組,剩下的 7 位則完全對應 7 位的 ASCII 碼。
  • 對於 n 位元組字符,第 1 個位元組的最高 n 位為 1 ,第 n+1 位為 0 ,並且其它位元組的最高兩位都為 10 。

下表展示了 UTF-8 與 Unicode 的轉換關係:

Unicode UTF-8
U+0000 – U+007F 0b0xxxxxxx
U+0080 – U+07FF 0b110xxxxx 10xxxxxx
U+0800 – U+FFFF 0b1110xxxx 10xxxxxx 10xxxxxx
U+10000 – U+10FFFF 0b11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 這種變長的形式完全兼容 ASCII ,這是它最大的特點。仔細觀察 UTF-8 的二進制位可以發現,UTF-8 甚至不需要 BOM ,因為除去單位元組的情況不考慮,多位元組情況下只有最高位元組的次高二進制位才是 1 。

UTF-8 目前是世界上使用最廣泛的 Unicode 編碼方案,幾乎所有的互聯網網站都使用 UTF-8 編碼,大多數 Linux 系統的默認編碼也為 UTF-8 。UTF-8 與 Unicode 一起,構成了現代編碼方案的基礎。

當然,UTF-8 也是有缺點的:儘管目前 UTF-8 可以使用一個到四個位元組靈活地表示所有的 Unicode 編碼,但是如果 Unicode 再往上添加,UTF-8 也不可避免地需要再次添加長度,甚至最長需要 6 個位元組才能表示 UCS-4 的所有情況。不過,在可遇見的將來,人們都不需要這麼多字符。

Unicode 與編碼經過了一個很長的發展過程,人們為了統一編碼附出了許多努力。在此過程中出現了許多編碼方案的嘗試。為了保持編碼的兼容,也付出過慘痛的代價:Windows 系統為了讓二十年前還沒有使用 UTF-8 編碼的程序不出現亂碼的情況,至今系統的默認編碼都不是 UTF-8 。

Unicode 也是在不斷發展的,除了繼續向 Unicode 添加字符以外,也有嘗試過多種 Unicode 的替代編碼方案。GB18030 是一種針對漢字的編碼改善方案,它在完全兼容 GB2313(部分兼容 GBK )的同時,也添加了對 Unicode 的支持。總之,為了規範編碼,人們還有很長的一段路要走。

轉自:

//frozencandles.fun/archives/290

參考資料/延伸閱讀

//home.unicode.org/

//en.wikipedia.org/wiki/Universal_Coded_Character_Set

//en.wikipedia.org/wiki/Unicode

//baike.baidu.com/item/%E7%BB%9F%E4%B8%80%E7%A0%81/2985798

//en.wikipedia.org/wiki/UTF-16

//en.wikipedia.org/wiki/UTF-8

//en.wikipedia.org/wiki/Byte_order_mark

//unicode.org/faq/utf_bom.html

//www.ibm.com/docs/en/aix/7.2?topic=globalization-code-sets-multicultural-support

Tags: