徹底搞懂編碼 GBK 和 UTF8

徹底搞懂編碼 GBK 和 UTF8

首先來看一下常用的編碼有哪些,截圖自Notepad++。其中ANSI在中國大陸即爲GBK(以前是GB2312),最常用的是 GBK 和 UTF8無BOM 編碼格式。後面三個都是有BOM頭的文本格式,UCS-2即爲人們常說的Unicode編碼,又分爲大端、小端。

所謂BOM頭(Byte Order Mark)就是文本文件中開始的幾個並不表示任何字符的字節,用二進制編輯器(如bz.exe)就能看到了。

  1. UTF8的BOM頭爲 0xEF 0xBB 0xBF

  2. Unicode大端模式爲 0xFE 0xFF

  3. Unicode小端模式爲 0xFF 0xFE

何爲GBK,何爲GB2312,與區位碼有何淵源?

區位碼是早些年(1980)中國製定的一個編碼標準,如果有玩過小霸王學習機的話,應該會記得有個叫做“區位”的輸入法(沒記錯的話是按F4選擇)。就是打四個數字然後就出來漢字了,什麼原理呢。請看下面的區位碼錶,每一個字符都有對應一個編號。其中前兩位爲“區”,後兩位爲“位”,中文漢字的編號區號是從16開始的,位號從1開始。前面的區號有一些符號、數字、字母、注音符號(臺)、製表符、日文等等。

而GB2312編碼就是基於區位碼的,用雙字節編碼表示中文和中文符號。一般編碼方式是:0xA0+區號,0xA0+位號。如下表中的 “安”,區位號是1618(十進制),那麼“安”字的GB2312編碼就是 0xA0+16  0xA0+18 也就是 0xB0 0xB2 。根據區位碼錶,GB2312的漢字編碼範圍是0xB0A1~0xF7FE

區位碼錶節選

可能大家注意到了,區位碼裏有英文和數字,按道理說是不是也應該是雙字節的呢。而一般情況下,我們見到的英文和數字是單字節的,以ASCII編碼,也就是說現代的GBK編碼是兼容ASCII編碼的。比如一個數字2,對應的二進制是0x32,而不是 0xA3 0xB2。那麼問題來了,0xA3 0xB2 又對應到什麼呢?還是2(笑)。注意看了,這裏的2跟2是不是有點不太一樣?!確實是不一樣的。這裏的雙字節2是全角的二,ASCII的2是半角的二,一般輸入法裏的切換全角半角就是這裏不同。

如果留意過早些年的手機(功能機),會發現人名中常見的“燊”字是打不出來的。爲什麼呢?因爲早期的區位碼錶裏面並沒有這些字,也就是說早期的GB2312也是沒有這些字的。到後來的GBK(1995)才補充了大量的漢字進去,當然現在的安卓蘋果應該都是GBK字庫了。再看看這些補充的漢字的字節碼 燊 0x9F 0xF6 。和前面說到的GB2312不同,有的字的編碼比 0xA0 0xA0 還小,難道新補充的區位號還能是負的??其實不然,這次的補充只補充了計算機編碼表,並沒有補充區位碼錶。也就是說區位碼錶並沒有更新,用區位碼打字法還是打不出這些字,而網上的反向區位碼錶查詢也只是按照GBK的編碼計算,並不代表字與區位號完全對應。時代的發展,區位碼錶早已經是進入博物館的東西了。

Big5是與GB2312同時期的一種臺灣地區繁體字的編碼格式。後來GBK編碼的制定,把Big5用的繁體字也包含進來(但編碼不兼容),還增加了一些其它的中文字符。細心的朋友可能還會發現,臺灣香港用的繁體字(如KTV裏的字幕)跟大陸用的繁體字還有點筆畫上的不一樣,其實這跟編碼無關,是字體的不同,大陸一般用的是宋體楷體黑體,港澳臺常用的是明體(鳥哥Linux私房菜用的是新細明體)。GBK總體編碼範圍爲0x8140~0xFEFE,首字節在 0x81~0xFE 之間,尾字節在 0x40~0xFE 之間,剔除 xx7F 一條線。詳細編碼表可以參考這個列表。微軟Windows安排給GBK的code page(代碼頁)是CP936,所以有時候看到編碼格式是CP936,其實就是GBK的意思。2000年和2005年,國家又先後兩次發佈了GB18030編碼標準,兼容GBK,新增四字節的編碼,但比較少見。

同一個編碼文件裏,怎麼區分ASCII和中文編碼呢?從ASCII表我們知道標準ASCII只有128個字符,0~127即0x00~0x7F(0111 1111)。所以區分的方法就是,高字節的最高位爲0則爲ASCII,爲1則爲中文。

UTF8編碼 與 Unicode編碼

GBK是中國標準,只在中國使用,並沒有表示大多數其它國家的編碼;而各國又陸續推出各自的編碼標準,互不兼容,非常不利於全球化發展。於是後來國際組織發行了一個全球統一編碼表,把全球各國文字都統一在一個編碼標準裏,名爲Unicode。很多人都很疑惑,到底UTF8與Unicode兩者有什麼關係?如果要類比的話,UTF8相當於GB2312,Unicode相當於區位碼錶,不同的是它們之間的編號範圍和轉換公式。那什麼是原始的Unicode編碼呢?如果你用過PHP的話,json_encode函數默認會把中文編碼成爲Unicode,比如“首發於博客園”就會轉碼成“\u9996\u53d1\u4e8e\u535a\u5ba2\u56ed”。可以看到每個字都變成了 \uXXXX 的形式,這個就是文字的對應Unicode編碼,\u表示Unicode的意思,網上也有用U+表示unicode。現行的Unicode編碼標準裏,絕大多數程序語言只支持雙字節。英文字母、標點也收納在Unicode編碼中。有興趣的可以在站長工具裏嘗試“中文轉Unicode”,可以得到你輸入文字的Unicode編碼。

因爲英文字符也全部使用雙字節,存儲成本和流量會大大地增加,所以Unicode編碼大多數情況並沒有被原始地使用,而是被轉換編碼成UTF8。下表就是其轉換公式:

第一種:Unicode從 0x0000 到 0x007F 範圍的,是不是有點熟悉?對,其實就是標準ASCII碼裏面的內容,所以直接去掉前面那個字節 0x00,使用其第二個字節(與ASCII碼相同)作爲其編碼,即爲單字節UTF8。

第二種:Unicode從 0x0080 到 0x07FF 範圍的,轉換成雙字節UTF8。

第三種:Unicode從 0x8000 到 0xFFFF 範圍的,轉換成三字節UTF8,一般中文都是在這個範圍裏。

第四種:超過雙字節的Unicode目前還沒有廣泛支持,僅見emoji表情在此範圍。

例如“博”字的Unicode編碼是\u535a。0x535A在0x0800~0xFFFF之間,所以用3字節模板 1110yyyy 10yyyyxx 10xxxxxx。將535A寫成二進制是:0101 0011 0101 1010,高八位分別代替y,低八位分別代替x,得到 11100101 10001101 10011010,也就是 0xE58D9A ,這就是博字的UTF8編碼。

前面提到,GBK的編碼裏英文字符有全角和半角之分,全角爲GBK的標準編碼過的雙字節2,半角爲ASCII的單字節2。那現在UTF8是全部用一個公式,理論上只有半角的2的,怎麼支持全角的2呢?哈哈,結果是Unicode爲中國特色的全角英文字符也單獨分配了編碼,簡單粗暴。比如全角的2的Unicode編碼是 \uFF12,轉換到UTF8就是 0xEFBC92。

文章開頭有說到 UCS-2,其實UCS-2就是原始的雙字節Unicode編碼,用二進制編輯器打開UCS-2大端模式的文本文件,從左往右看,看到的就是每個字符的Unicode編碼了。至於什麼是大端小端,就是字節的存放順序不同,這一般是嵌入式編程的範疇。

如何區分一個文本是無BOM的UTF8還是GBK

前面說到的幾種編碼,其中有的是有BOM頭的,可以直接根據BOM頭區分出其編碼。有兩個是沒有BOM頭的,UTF8和GBK,那麼兩者怎麼區分呢?答案是,只能按大量的編碼分析來區分。目前識別準確率很高的有:Notepad++等一些常用的IDE,PHP的mb_系列函數,python的chardet庫及其它語言衍生版如jchardet,jschardet 等(請自行github)。

那麼這些庫是怎麼區分這些編碼的呢?那就是詞庫,你會看到庫的源碼裏有大量的數組,其實就是對應一個編碼裏的常見詞組編碼組合。同樣的文件字節流在一個詞組庫裏的匹配程度越高,就越有可能是該編碼,判斷的準確率就越大。而文件中的中文越少越零散,判斷的準確率就越低。

關於ASCII

文中多次提及ASCII編碼,其實這應該是每個程序員都非常熟悉、認真瞭解的東西。對於嵌入式開發的人來說,應該能隨時在字符與ASCII碼中轉換,就像十六進制與二進制之間的轉換一樣。標準ASCII是128個,範圍是0x00~0x7F (0000 0000~0111 0000) ,最高位爲0。也有一個擴展ASCII碼規則,把最高位也用上了,變成256個,但是這個擴展標準爭議很大,沒有得到推廣,應該以後不會得到推廣。因爲無論是GBK還是UTF8,如果ASCII字符編碼最高位能爲1都會造成混亂無法解析。

以GBK爲例,如果ASCII的字符最高位也能是1,那麼是應該截取一個解析爲ASCII呢?還是截取兩個解析爲中文字符?這根本無法判斷。UTF8也是同理,遇到 0xxx 開頭則截取一個(即爲標準ASCII), 遇到 110x 開頭則截取兩個,遇到 1110 開頭則截取三個,如果ASCII包含1開頭的,則無法確定何時截取多少個。

在哪裏還能一睹擴展ASCII的真容呢?其實很簡單,只要把網頁的meta改成ASCII就行了 <meta charset="ASCII" /> 。又或者瀏覽器的編碼選擇“西方”,即可見到與平常所見不同的亂碼。(截圖爲火狐)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章