一文講透Windows平臺下的ASCII,Unicode編碼問題

wangh96875發表於2024-11-13

ascii

控制字元的編號範圍是0-31和127(0x00-0x1F和0x7F),共33個字元。

可顯示字元編號範圍是32-126(0x20-0x7E),共95個字元。

((20241112221251-kkgxrg6 "標準 ASCII 碼對照表"))

UNICODE

美國人意識到他們應該提出一種標準方案來展示世界上所有語言中的所有字元,出於這個目的,Unicode 誕生了。

Unicode 源於一個很簡單的想法:將全世界所有的字元包含在一個集合裡,計算機只要支援這一個字符集,就能顯示所有的字元,再也不會有亂碼了。

它從 0 開始,為每個符號指定一個編號,這叫做”碼點”(code point)。比如,碼點 0 的符號就是 null(表示所有二進位制位都是 0)。

U+0000 = null

上式中,U+表示緊跟在後面的十六進位制數是 Unicode 的碼點。

這麼多符號,Unicode 不是一次性定義的,而是分割槽定義。每個區可以存放 65536 個(2^16)字元,稱為一個平面(plane)。目前,一共有 17 個平面,也就是說,整個 Unicode 字符集的大小現在是 2^21。

最前面的 65536 個字元位,稱為基本平面(縮寫 BMP),它的碼點範圍是從 0 一直到 2^16-1,寫成 16 進位制就是從 U+0000 到 U+FFFF。所有最常見的字元都放在這個平面,這是 Unicode 最先定義和公佈的一個平面。

剩下的字元都放在輔助平面(縮寫 SMP),碼點範圍從 U+010000 一直到 U+10FFFF。

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  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 ^ 4+1)平面,也就是說,整個 Unicode 字符集的大小現在是 2 ^20 + 2 ^16。

最前面的 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的方法

①、將增補字元的碼點值減去0x10000,得到一個20位長的二進位制數

②、將得到的20位長二進位制數拆分為高10位位元和低10位位元

③、20位長的高10位位元加上0xD800得到第一個代理碼點,即高代理碼點

④、20位長的低10位位元加上0xDC00得到第二個代理碼點,即低代理碼點

⑤、將得到的高代理碼點和低代理碼點組合成“代理對”,便得到了增補字元的UTF-16編碼

⑥、示例:求增補平面碼點值為U+10437的UTF-16編碼

將0x10437減去0x10000,得到0x00437,二進位制為0000 0000 0100 0011 0111

將高10位,即0000 0000 01加上0xD800(二進位制為1101 1000 0000 0000),得到高代理碼點為:0xD801(二進位制為1101 1000 0000 0001)

將低10位,即00 0011 0111加上0xDC00(二進位制為1101 1100 0000 0000),得到低代理碼點為:0xDC37(二進位制為1101 1100 0011 0111)

Python編碼相關程式碼片段

>>> '漢'.encode('utf-8') # 獲取漢的utf-8編碼
b'\xe6\xb1\x89'
>>> ord('漢') #獲取漢的unicode碼點
27721
>>> hex(ord('漢')) #16進製表示unicode碼點
'0x6c49'
>>> bin(ord('漢')) #二進位制表示unicode碼點
'0b110110001001001'

記事本TXT編碼問題解釋

記事本檔案儲存文字時,討論一下三種編碼格式,ANSI,Unicode和UTF-8,需要指出的是,此處的Unicode表示UTF-16 小端

如果是英文a,用ascii儲存,查((20241112221251-kkgxrg6 "標準 ASCII 碼對照表")),是0x61

如果是用Unicode儲存,即為0x6100,0xFFFE為UTF16小端編碼

如果是Unicode big endian儲存,即為0x0061,FEFF為UTF16大端編碼

如果是UTF8儲存,即為0x61,EFBBBF為BOM頭

中文"漢"字,unicode碼是0x6c49

如果以ascii碼儲存,由於中文字不能以ascii儲存,會以系統的編碼儲存,可以在cmd視窗中chcp檢視預設編碼,936代表簡體中文(GB2312)

如果要改預設編碼集,可以在下圖勾選以utf-8作為預設編碼

0xBABA表示“漢”字在gb2312的編碼,可以在這裡檢視:漢字字符集編碼查詢;中文字符集編碼:GB2312、BIG5、GBK、GB18030、Unicode

如果以unicode碼儲存,6c是高位,49是低位

如果是Unicode big endian儲存

如果是utf8儲存,0x6c49,二進位制為 110110001001001

按照這條轉換

0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx

轉換為:11100110 10110001 10001001,十六進位制為0xE6B189

對於特殊字:𫝀,Unicode碼點是:\u0002B740,二進位制為00000000 00000010 10110111 01000000

utf8編碼,使用這個規則

0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

二進位制為:11110000 10101011 10011101 10000000,十六進位制為\uF0AB9D80

轉成unicode的方法

①、將增補字元的碼點值減去0x10000,得到一個20位長的二進位制數,即2B740-10000=1B740

②、將得到的20位長二進位制數拆分為高10位位元和低10位位元,0001101101 1101000000

③、20位長的高10位位元加上0xD800得到第一個代理碼點,即高代理碼點,D86D

④、20位長的低10位位元加上0xDC00得到第二個代理碼點,即低代理碼點,DF40

小端是:6DD840DF

大端是:D86DDF40

相關文章