1. 程式人生 > >計算機字元編碼詳解——從理論到實踐

計算機字元編碼詳解——從理論到實踐

前言


最近在看《深入理解計算機系統》,讀到“字元編碼”時不禁想起了初學時那段痛苦的歲月,同時又沒找到一篇將理論和實踐結合在一起的文章,為此決定自己寫一份。希望能把我走過的彎路總結出來,能幫助一些還在路上的朋友。
關於計算機如何儲存資訊,請參考《深入理解計算機系統》的第 02 章內容,此文只講解與字元編碼有關的內容。
另外,關於常用編輯器對於字元編碼的區別,請參考我的另一篇文件——《Win記事本、Sublime、Notepad++等編輯器對常見字元編碼的處理和區別:GB2312、GBK、ANSI、Unicode、UTF-8》,此處也不再贅述。

字元的表示原理


簡單說,計算機內所有資訊都是使用0和1進行表示的

對於一個短路來說,0代表關,1代表開。那把這些電路組合起來就可以有長串0和1組成的二進位制數字,我們對這些數字進行編碼和解碼,我們就能用它來表示我們想要表示的東西了。比如:文字、影象、視訊等等,就是一組0和1的二進位制序列。
二進位制數的每一個位表示一個計算機位(bit,簡稱位),8個位組成一個位元組(byte)。那麼一個位元組可以表示256種含義(2*2*2*2*2*2*2*2=256)。

雖然機器是基於二進位制的,但對人類來說,因為二進位制數太長了,需要做精簡。因此需要將其轉換成十六進位制(hexadecimal,簡稱hex)。轉換方式很簡單,使用“8421法”將四位二進位制數轉換成十六進位制數的一位,比如:1010(binary)會轉為A(hex)。在 C 語言中,十六進位制數以”0x”或“0X”開頭,A表示10,F表示16。

此後,00000000~11111111就可以用0x00~0xFF來表示了。

常見的字元編碼

ANSI是美國國標的英文字元編碼,佔用一個位元組,收錄了127個字元。
原理很簡單,用一個位元組的不同的值表示一個字元、字母或數字。

文字開頭有不可見的標誌碼,用於告知下文應該以什麼方式解碼。
文中所有字元編碼都是這種方式。

英文字元編碼佔用一個位元組就夠了,但對於像中文、日文這種象形文字國家一個位元組就肯定不夠了。為此,中國的國標字元編碼需要佔用 2 個位元組,中國標準局依次釋出了GB2312、GBK、GB18030等。

雖然每個國家或地區的字元編碼自行搞定了,但是多個國家間還是不能相容。為此Unicode編碼應運而生,它整合全世界的幾乎所有語言文字。

下面我們依次講解下ANSI、GB2312、GBK、GB18030、Unicode、UTF-8等編碼。

ANSI編碼

ANSI碼佔用1個位元組,收錄127個字元,取值範圍0x00~0x7F

ANSI編碼表:
這裡寫圖片描述

GB2312、GBK、GB18030

釋出順序依次為:GB2312、GBK、GB18030。都以ANSI格式儲存

彼此區別:

  • GB2312是常用簡體漢字;
  • GBK基於GB2312,包含更多生僻字,支援簡繁雙體
  • GB18030基於GBK,包含更多生僻字,支援少數民族文字,支援CJK(中日韓)統一字元。
  • GB2312和GBK都是雙位元組,GB18030分單、雙、四位元組三個部分。


比如:在GB2312上沒有“喆”,在GBK上才被收錄。

因這三種編碼規則差不多,下面我們重點講解下GB2312。

GB2312

考慮到以下因素:

  • ANSI範圍是:0x00~0x7F。
  • 國際通用精確數字計算使用的BCD編碼範圍是:0x00~0x99。

為避免衝突,要預留一定空間,GB2312的範圍:0xA0~0xFF

GB2312:

  • 又稱GB0,國標局釋出,1981年5月1日實施。
  • 收錄6763個漢字:一級漢字3755個,常用字,排在前面;二級漢字3008個,生僻字,排在後面。
  • GB2312是一種區位碼。分為94個區(01-94),每區94個字元(01-94)。
    • 01-09區為特殊符號;
    • 10-15區沒有編碼;
    • 16-55區為一級漢字,按拼音排序,共3755個;
    • 56-87區為二級漢字,按部首/筆畫排序,共3008個;
    • 88-94區沒有編碼;
    • GB2312只是編碼表,在計算機中通常都是用”EUC-CN“表示法,即在每個區位加上0xA0來表示。區和位分別佔用一個位元組。

EUC-CN表示法:

  • EUC-CN是GB2312最常用的表示方法。
  • GB2312字元使用兩個位元組來表示, 每個位元組=0xA0+區位碼。
  • “第一位位元組”取值範圍:0xB0~0xF7
  • “第二位位元組”取值範圍:0xA1~0xFE

舉例來說,“啊”字是GB 2312之中的第一個漢字,它的區位碼是1601
在EUC-CN之中,它把0xA0+16=0xB0;0xA0+1=0xA1;得出0xB0A1

GB2312的中文區位的首末兩位是特意空出來的。原因是:

  • 首位(0x00):在ANSI中表示空,中國人更習慣從1開始計數,而不是從0開始計數。
  • 末位(0xFF):代表結束,因為有些程式設計師會將其定義為空,因此空出以避免衝突。

GB2312中文編碼頁的排列方式:
這裡寫圖片描述

《關於Windows記事本與Sublime Text對中文字元編碼轉換的問題》:

先對Windows記事本做個小實驗:

  1. 在Windows記事本中,新建檔案,輸入“你好hello1234”,以ANSI格式儲存後;
  2. 在Sublime Text中開啟此檔案,檔案格式為GB2312,輸入GB2312內不支援的漢字“喆”,提示儲存失敗,原因是’\u5586’是非法的多位元組序列;
    ’gb2312’ codec can’t encode character ‘\u5586’ in position 11: illegal multibyte sequence
    第 11 個字元就是“喆”。
    注意:如果不刪除這個欄位,Sublime Text會以UTF-8格式重新儲存這個檔案。

  3. 在Windows記事本中,輸入GB2312內不支援的漢字“喆”,按ANSI格式儲存成功;

  4. 再次用Sublime Text開啟此檔案,檔案格式已變為GBK。

綜上,在Windows記事本中,GB2312和GBK是統一以ANSI方式管理的。我猜測,Windows記事本預設使用GB2312儲存漢字,如遇到GB2312不能識別的生僻字才會使用GBK格式儲存。

Sublime Text並不支援這種功能,如果你強行輸入“喆”字以後,會有錯誤框彈出。然後,它會把這個檔案轉換為UTF-8格式並儲存。
因為Sublime Text預設是支援UTF-8編碼,不支援GB2312、GBK等中文編碼,需要安裝ConvertToUTF-8外掛才能實現。顯然,ConvertToUTF-8並沒考慮這種情況。

在Sublime Text中以GB2312格式無法儲存沒被收錄的字元:
在Sublime Text中以GB2312格式無法儲存沒被收錄的字元

Unicode簡介:

  • 統一碼、萬國碼、單一碼,包括字符集、編碼方案等。
  • 它為每種語言中的每個字元設定了統一併且唯一的二進位制編碼,以滿足跨語言、跨平臺進行文字轉換、處理的要求。
  • 1990年開始研發,1994年正式公佈。
  • 統一碼為每個字元而非字形定義唯一的編碼(即一個整數),字型視覺展示由其他軟體來處理。由此,可解決漢字一字多形的認定爭議(如“ɑ/a”、“戶/戶/戸”等)。

Unicode與UCS:
統一編碼聯盟都試圖獨立建立一套國際通用的字元編碼,此後雙方都意識到沒必要建立兩套編碼,於是兩者融合並承諾彼此相容。從Unicode 2.0開始,Unicode採用了與ISO 10646-1相同的字型檔和字碼。

通用字符集(Universal Character Set, UCS):由ISO制定的ISO 10646(或稱ISO/IEC 10646)標準所定義的標準字符集。UCS-2用兩個位元組編碼,UCS-4用4個位元組編碼。

簡單說,Unicode與UCS-2相同。

Unicode編碼原理:

UCS-4有4個位元組:

  • 第一個位元組:表示組(group),最高位為0,則有128個。
  • 第二個位元組:表示平面(plane),256個。
  • 第三個位元組:表示行(row),256個。
  • 第四個位元組:表示碼位(cell),256個。


group 0的是最底層的平面0被稱作BMP(Basic Multilingual Plane),即0x0000XXXX(大寫X表示任何一個十六進位制數字)。

平面 字元範圍 備註
基本多語言平面 0~0xFFFF BMP, or Plane 0
增補多語言平面 0x10000~0x1FFFF SMP, or Plane 1
增補表意字元平面 0x20000~0x2FFFF SIP, or Plane 2
增補專用平面 SSP, or Plane 14

注意:

  • BMP:Basic Multilingual Plan
  • SMP:Supplementary Multilingual Plane
  • SIP:Supplementary Ideographic Plane
  • SSP:Supplementary Special-purpose Plane

當UCS-4的前兩個位元組為全零(BMP)時,那麼去掉UCS-4的前兩個零位元組就得到了UCS-2。

在Unicode中,有幾種方式唯一編碼數字的轉換格式:UTF-8、UTF-16、UTF-32。
UTF(Unicode Transformation Format),可以翻譯成Unicode字符集轉換格式,即怎樣將Unicode定義的數字轉換成程式資料。


例如,“漢字”對應的數字是0x6c49和0x5b57,而編碼的程式資料是:

char      data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97};  //UTF-8編碼
char16_t data_utf16[]={0x6C49,0x5B57};                  //UTF-16編碼
char32_t data_utf32[]={0x00006C49,0x00005B57};          //UTF-32編碼

注: char16_t 和 char32_t 是 C++ 11 標準新增的關鍵字。如果你的編譯器不支援 C++ 11 標準,請改用 unsigned short 和 unsigned long。

“漢字”的UTF-8編碼需要6個位元組。
“漢字”的UTF-16編碼需要兩個char16_t,大小是4個位元組。
“漢字”的UTF-32編碼需要兩個char32_t,大小是8個位元組。

UTF-8

Unicode編碼(十六進位制) UTF-8 位元組流(二進位制)
00000000 - 0000007F 0xxxxxxx
00000080 - 000007FF 110xxxxx 10xxxxxx
00000800 - 0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
00010000 - 001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
00200000 - 03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
04000000 - 7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8的特點是對不同範圍的字元使用不同長度的編碼,相容ASCII編碼。

  • 對於0x00-0x7F之間的字元,UTF-8編碼與ASCII編碼完全相同。
  • UTF-8編碼的最大長度是6個位元組。
  • 從上表可以看出,6位元組模板有31個x,即可以容納31位二進位制數字。Unicode的最大碼位0x7FFFFFFF也只有31位。

例1:
“漢”的Unicode編碼是0x6C49,在0x0800-0xFFFF之間,使用3位元組模板:1110xxxx 10xxxxxx
10xxxxxx。 0x6C49的二進位制表示為:0110 1100 0100 1001,
用這個位元流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。


例2:Unicode編碼0x20C30在0x010000-0x10FFFF之間,使用4位元組模板:11110xxx 10xxxxxx
10xxxxxx 10xxxxxx。將0x20C30寫成21位二進位制數字(不足21位就在前面補0):0 0010 0000 1100
0011 0000,用這個位元流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0
A0 B0 B0。

UTF-16

根據位元組序的不同,UTF-16可以被實現為UTF-16BEUTF-16LE(Windows系統預設)。

Win32的API提供兩種呼叫方式:

  • 以W結尾的函式:使用UTF-16LE編碼模式;
  • 以A結尾的函式:使用多位元組編碼模式(中文編碼是以ANSI格式儲存的),但是Windows會在底層將其轉換為UTF-16LE編碼來處理。

因此對於Windows程式設計來說,最好預設使用Unicode(UTF-16LE)模式開發,避免系統底層多餘的字元轉換。

Unicode字元編碼範圍 UTF-16 單個字元長度 備註
U+0000~U+FFFF 1個16位編碼單元 16位無符號整數,固定寬度,BMP中的字元
U+10000~U+10FFFF 2個16位編碼單元 稱作代理對(surrogate pair),變寬

UTF-16是早期Unicode遺留下的歷史產物,原本被設計成具有固定寬度的16位編碼格式。為支援超過U+FFFF的增補字元,設立了代理機制。

編碼轉化規則

UTF-16編碼以16位無符號整數為單位。我們把Unicode編碼記作U。編碼規則如下:

  • 如果U<0x10000,U的UTF-16編碼就是U對應的16位無符號整數。
  • 如果U≥0x10000,
    • 我們先計算U’=U-0x10000,
    • 然後將U’寫成二進位制形式:yyyy yyyy yyxx xxxx xxxx,
    • U的UTF-16編碼(二進位制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

為什麼U’可以被寫成20個二進位制位?
Unicode的最大碼位是0x10FFFF,減去0x10000後,U’的最大值是0xFFFFF,2的5次冪是32,所以肯定可以用20個二進位制位表示。

例如:Unicode編碼0x20C30,減去0x10000後,得到0x10C30,寫成二進位制是:0001 0000 1100 0011
0000。用前10位依次替代模板中的y,用後10位依次替代模板中的x,就得到:1101100001000011
1101110000110000,即0xD843 0xDC30。

UTF-32

根據位元組序的不同,UTF-32可以被實現為UTF-32LE或UTF-32BE。

UTF-32是一種最簡單的Unicode編碼格式。每個Unicode碼點被直接表示為一個32位的編碼單元。UTF-32是一種固定寬度的字元編碼格式。
每個UTF-32編碼單元的值與Unicode碼點的值完全相同。

因為對於多數情況下,非常浪費空間,因此應用場景很少。

各編碼間的轉換

總結

參考文獻