我們在處理文件、瀏覽網頁、編寫程式時,時不時會碰到亂碼的情況。亂碼幾乎總是令人心煩,讓人困惑。希望通過本節和下節文章,你可以自信從容地面對亂碼,恢復亂碼。 談亂碼,我們就要談數據的二進位表示,我們已經在前兩節談過整數和小數的二進位表示,接下了我們將討論字元和文本的二進位表示。 由於內容比較多,我們將 ...
我們在處理文件、瀏覽網頁、編寫程式時,時不時會碰到亂碼的情況。亂碼幾乎總是令人心煩,讓人困惑。希望通過本節和下節文章,你可以自信從容地面對亂碼,恢復亂碼。
談亂碼,我們就要談數據的二進位表示,我們已經在前兩節談過整數和小數的二進位表示,接下了我們將討論字元和文本的二進位表示。
由於內容比較多,我們將分兩節來介紹。本節主要介紹各種編碼,亂碼產生的原因,以及簡單亂碼的恢復。下節我們介紹複雜亂碼的恢復。
編碼和亂碼聽起來比較複雜,文章也比較長,但其實並不複雜,請耐心閱讀,讓我們逐步來探討。
ASCII
世界上雖然有各種各樣的字元,但電腦發明之初沒有考慮那麼多,基本上只考慮了美國的需求,美國大概只需要128個字元,美國就規定了這128個字元的二進位表示方法。
這個方法是一個標準,稱為ASCII編碼,全稱是American Standard Code for Information Interchange,美國信息互換標準代碼。
128個字元用7個位剛好可以表示,電腦存儲的最小單位是byte,即8位,ASCII碼中最高位設置為0,用剩下的7位表示字元。這7位可以看做數字0到127,ASCII碼規定了從0到127個,每個數字代表什麼含義。
我們先來看數字32到126的含義,如下圖所示,除了中文之外,我們平常用的字元基本都涵蓋了,鍵盤上的字元大部分也都涵蓋了。
數字32到126表示的這些字元都是可列印字元,0到31和127表示一些不可以列印的字元,這些字元一般用於控制目的,這些字元中大部分都是不常用的,下表列出了其中相對常用的字元。
Ascii 碼對美國是夠用了,但對別的國家而言卻是不夠的,於是,各個國家的各種電腦廠商就發明瞭各種各種的編碼方式以表示自己國家的字元,為了保持與Ascii 碼的相容性,一般都是將最高位設置為1。也就是說,當最高位為0時,表示Ascii碼,當為1時就是各個國家自己的字元。
在這些擴展的編碼中,在西歐國家中流行的是ISO 8859-1和Windows-1252,在中國是GB2312,GBK,GB18030和Big5,我們逐個來看下這些編碼。
ISO 8859-1
ISO 8859-1又稱Latin-1,它也是使用一個位元組表示一個字元,其中0到127與Ascii一樣,128到255規定了不同的含義。
在128到255中,128到159表示一些控制字元,這些字元也不常用,就不介紹了。160到255表示一些西歐字元,如下圖所示:
Windows-1252
ISO 8859-1雖然號稱是標準,用於西歐國家,但它連歐元(€) 這個符號都沒有,因為歐元比較晚,而標準比較早。實際使用中更為廣泛的是Windows-1252編碼,這個編碼與ISO8859-1基本是一樣的,區別 只在於數字128到159,Windows-1252使用其中的一些數字表示可列印字元,這些數字表示的含義,如下圖所示:
這個編碼中加入了歐元符號以及一些其他常用的字元。基本上可以認為,ISO 8859-1已被Windows-1252取代,在很多應用程式中,即使文件聲明它採用的是ISO 8859-1編碼,解析的時候依然被當做Windows-1252編碼。
HTML5
甚至明確規定,如果文件聲明的是ISO 8859-1編碼,它應該被看做Windows-1252編碼。為什麼要這樣呢?因為大部分人搞不清楚ISO
8859-1和Windows-1252的區別,當他說ISO
8859-1的時候,其實他實際指的是Windows-1252,所以標準乾脆就這麼強制了。
GB2312
美國和西歐字元用一個位元組就夠了,但中文顯然是不夠的。中文第一個標準是GB2312。
GB2312標準主要針對的是簡體中文常見字元,包括約7000個漢字,不包括一些罕用詞,不包括繁體字。
GB2312固定使用兩個位元組表示漢字,在這兩個位元組中,最高位都是1,如果是0,就認為是Ascii字元。在這兩個位元組中,其中高位位元組範圍是0xA1-0xF7,低位位元組範圍是0xA1-0xFE。
比如,"老馬"的GB2312編碼是(16進位表示):
老 | 馬 |
C0 CF | C2 ED |
GBK
GBK建立在GB2312的基礎上,向下相容GB2312,也就是說,GB2312編碼的字元和二進位表示,在GBK編碼里是完全一樣的。
GBK增加了一萬四千多個漢字,共計約21000漢字,其中包括繁體字。
GBK同樣使用固定的兩個位元組表示,其中高位位元組範圍是0x81-0xFE,低位位元組範圍是0x40-0x7E和0x80-0xFE。
需要註意的是,低位位元組是從0x40也就是64開始的,也就是說,低位位元組最高位可能為0。那怎麼知道它是漢字的一部分,還是一個Ascii字元呢?
其實很簡單,因為漢字是用固定兩個位元組表示的,在解析二進位流的時候,如果第一個位元組的最高位為1,那麼就將下一個位元組讀進來一起解析為一個漢字,而不用考慮它的最高位,解析完後,跳到第三個位元組繼續解析。
GB18030
GB18030向下相容GBK,增加了五萬五千多個字元,共七萬六千多個字元。包括了很多少數民族字元,以及中日韓統一字元。
用兩個位元組已經表示不了GB18030中的所有字元,GB18030使用變長編碼,有的字元是兩個位元組,有的是四個位元組。
在兩位元組編碼中,位元組表示範圍與GBK一樣。在四位元組編碼中,第一個位元組的值從0x81到0xFE,第二個位元組的值從0x30到0x39,第三個位元組的值從0x81到0xFE,第四個位元組的值從0x30到0x39。
解析二進位時,如何知道是兩個位元組還是四個位元組表示一個字元呢?看第二個位元組的範圍,如果是0x30到0x39就是四個位元組表示,因為兩個位元組編碼中第二位元組都比這個大。
Big5
Big5是針對繁體中文的,廣泛用於臺灣香港等地。
Big5包括1萬3千多個繁體字,和GB2312類似,一個字元同樣固定使用兩個位元組表示。在這兩個位元組中,高位位元組範圍是0x81-0xFE,低位位元組範圍是0x40-0x7E和0xA1-0xFE。
編碼彙總
我們簡單彙總一下上面的內容。
Ascii碼是基礎,一個位元組表示,最高位設為0,其他7位表示128個字元。其他編碼都是相容Ascii的,最高位使用1來進行區分。
西歐主要使用Windows-1252,使用一個位元組,增加了額外128個字元。
中文大陸地區的三個主要編碼GB2312,GBK,GB18030,有時間先後關係,表示的字元數越來越多,且後面的相容前面的,GB2312和GBK都是用兩個位元組表示,而GB18030則使用兩個或四個位元組表示。
香港臺灣地區的主要編碼是Big5。
如果文本里的字元都是Ascii碼字元,那麼採用以上所說的任一編碼方式都是一一樣的。
但如果有高位為1的字元,除了GB2312/GBK/GB18030外,其他編碼都是不相容的,比如,Windows-1252和中文的各種編碼是不相容的,即使Big5和GB18030都能表示繁體字,其表示方式也是不一樣的,而這就會出現所謂的亂碼。
初識亂碼
一個法國人,採用Windows-1252編碼寫了個文件,發送給了一個中國人,中國人使用GB18030來解析這個字元,看到的就是亂碼,我們舉個例子:
法 國人發送的是 "Pékin",Windows-1252的二進位是(採用16進位):50 E9 6B 69 6E,第二個位元組E9對應é,其他都是Ascii碼,中國人收到的也是這個二進位,但是他把它看做成了GB18030編碼,GB18030中E9 6B對應的是字元"閗i",於是他看到的就是:"P閗in",這看來就是一個亂碼。
反之也是一樣的,一個GB18030編碼的文件如果被看做Windows-1252也是亂碼。
這 種情況下,之所以看起來是亂碼,是因為看待或者說解析數據的方式錯了。糾正的方式,只要使用正確的編碼方式進行解讀就可以了。很多文件編輯器,如 EditPlus, NotePad++, UltraEdit都有切換查看編碼方式的功能,瀏覽器也都有切換查看編碼方式的功能,如Firefox,在菜單 "查看"->"文字編碼"中。
切換查看編碼的方式,並沒有改變數據的二進位本身,而只是改變瞭解析數據的方式,從而改變了數據看起來的樣子。(稍後我們會提到編碼轉換,它正好相反)。
很多時候,做這樣一個編碼查看方式的切換,就可以解決亂碼的問題。但有的時候,這樣是不夠的,我們稍後提到。
Unicode
以上我們介紹了中文和西歐的字元與編碼,但世界上還有很多別的國家的字元,每個國家的各種電腦廠商都對自己常用的字元進行編碼,在編碼的時候基本忽略了別的國家的字元和編碼,甚至忽略了同一國家的其他電腦廠商,這樣造成的結果就是,出現了太多的編碼,且互相不相容。
世界上所有的字元能不能統一編碼呢?可以,這就是Unicode。
Unicode 做了一件事,就是給世界上所有字元都分配了一個唯一的數字編號,這個編號範圍從0x000000到0x10FFFF,包括110多萬。但大部分常用字元都 在0x0000到0xFFFF之間,即65536個數字之內。每個字元都有一個Unicode編號,這個編號一般寫成16進位,在前面加U+。大部分中文 的編號範圍在U+4E00到U+9FA5,例如,"馬"的Unicode是U+9A6C。
Unicode就做了這麼 一件事,就是給所有字元分配了唯一數字編號。它並沒有規定這個編號怎麼對應到二進位表示,這是與上面介紹的其他編碼不同的,其他編碼都既規定了能表示哪些 字元,又規定了每個字元對應的二進位是什麼,而Unicode本身只規定了每個字元的數字編號是多少。
那編號怎麼對應到二進位表示呢?有多種方案,主要有UTF-32, UTF-16和UTF-8。
UTF-32
這個最簡單,就是字元編號的整數二進位形式,四個位元組。
但有個細節,就是位元組的排列順序,如果第一個位元組是整數二進位中的最高位,最後一個位元組是整數二進位中的最低位,那這種位元組序就叫“大端”(Big Endian, BE),否則,正好相反的情況,就叫“小端”(Little Endian, LE)。對應的編碼方式分別是UTF-32BE和UTF-32LE。
可以看出,每個字元都用四個位元組表示,非常浪費空間,實際採用的也比較少。
UTF-16
UTF-16使用變長位元組表示:
- 對於編號在U+0000到U+FFFF的字元 (常用字元集),直接用兩個位元組表示。需要說明的是,U+D800到U+DBFF之間的編號其實是沒有定義的。
- 字元值在U+10000到U+10FFFF之間的字元(也叫做增補字元集),需要用四個位元組表示。前兩個位元組叫高代理項,範圍是U+D800到 U+DBFF,後兩個位元組叫低代理項,範圍是U+DC00到U+DFFF。數字編號和這個二進位表示之間有一個轉換演算法,本文就不介紹了。
區分是兩個位元組還是四個位元組表示一個符號就看前兩個位元組的編號範圍,如果是U+D800到U+DBFF,就是四個位元組,否則就是兩個位元組。
UTF-16也有和UTF-32一樣的位元組序問題,如果高位存放在前面就叫大端(BE),編碼就叫UTF-16BE,否則就叫小端,編碼就叫UTF-16LE。
UTF-16常用於系統內部編碼,UTF-16比UTF-32節省了很多空間,但是任何一個字元都至少需要兩個位元組表示,對於美國和西歐國家而言,還是很浪費的。
UTF-8
UTF-8就是使用變長位元組表示,每個字元使用的位元組個數與其Unicode編號的大小有關,編號小的使用的位元組就少,編號大的使用的位元組就多,使用的位元組個數從1到4個不等。
具體來說,各個Unicode編號範圍對應的二進位格式如下圖所示:
圖中的x表示可以用的二進位位,而每個位元組開頭的1或0是固定的。
小於128的,編碼與Ascii碼一樣,最高位為0。其他編號的第一個位元組有特殊含義,最高位有幾個連續的1表示一共用幾個位元組表示,而其他位元組都以10開頭。
對於一個Unicode編號,具體怎麼編碼呢?首先將其看做整數,轉化為二進位形式(去掉高位的0),然後將二進位位從右向左依次填入到對應的二進位格式x中,填完後,如果對應的二進位格式還有沒填的x,則設為0。
我們來看個例子,'馬'的Unicode編號是:0x9A6C,整數編號是39532,其對應的UTF-8二進位格式是:
1110xxxx 10xxxxxx 10xxxxxx
整數編號39532的二進位格式是 1001 101001 101100
將這個二進位位從右到左依次填入二進位格式中,結果就是其UTF-8編碼:
11101001 10101001 10101100
16進位表示為:0xE9A9AC
和UTF-32/UTF-16不同,UTF-8是相容Ascii的,對大部分中文而言,一個中文字元需要用三個位元組表示。
Uncode編碼小結
Unicode給世界上所有字元都規定了一個統一的編號,編號範圍達到110多萬,但大部分字元都在65536以內。Unicode本身沒有規定怎麼把這個編號對應到二進位形式。
UTF- 32/UTF-16/UTF-8都在做一件事,就是把Unicode編號對應到二進位形式,其對應方法不同而已。UTF-32使用4個位元組,UTF-16 大部分是兩個位元組,少部分是四個位元組,它們都不相容Ascii編碼,都有位元組順序的問題。UTF-8使用1到4個位元組表示,相容Ascii編碼,英文字元 使用1個位元組,中文字元大多用3個位元組。
編碼轉換
有了Unicode之後,每一個字元就有了多種不相容的編碼方式,比如說"馬"這個字元,它的各種編碼方式對應的16進位是:
GB18030 | C2 ED |
Unicode編號 | 9A 6C |
UTF-8 | E9 A9 AC |
UTF-16LE | 6C 9A |
這幾種格式之間可以藉助Unicode編號進行編碼轉換。可以簡化認為,每種編碼都有一個映射表,存儲其特有的字元編碼和Unicode編號之間的對應關係,這個映射表是一個簡化的說法,實際上可能是一個映射或轉換方法。
編碼轉換的具體過程可以是,比如說,一個字元從A編碼轉到B編碼,先找到字元的A編碼格式,通過A的映射表找到其Unicode編號,然後通過Unicode編號再查B的映射表,找到字元的B編碼格式。
舉例來說,"馬"從GB18030轉到UTF-8,先查GB18030->Unicode編號表,得到其編號是9A 6C,然後查Uncode編號->UTF-8表,得到其UTF-8編碼:E9 A9 AC。
與前文提到的切換查看編碼方式正好相反,編碼轉換改變了數據的二進位格式,但並沒有改變字元看上去的樣子。
再看亂碼
在前文中,我們提到亂碼出現的一個重要原因是解析二進位的方式不對,通過切換查看編碼的方式就可以解決亂碼。
但如果怎麼改變查看方式都不對的話,那很有可能就不僅僅是解析二進位的方式不對,而是文本在錯誤解析的基礎上還進行了編碼轉換。
我們舉個例子來說明:
- 兩個字 "老馬",本來的編碼格式是GB18030,編碼是(16進位): C0 CF C2 ED。
- 這個二進位形式被錯誤當成了Windows-1252編碼, 解讀成了字元 "ÀÏÂí"
- 隨後這個字元進行了編碼轉換,轉換成了UTF-8編碼,形式還是"ÀÏÂí",但二進位變成了:C3 80 C3 8F C3 82 C3 AD,每個字元兩個位元組。
- 這個時候,再按照GB18030解析,字元就變成了亂碼形式"脌脧脗鉚", 而且這時無論怎麼切換查看編碼的方式,這個二進位看起來都是亂碼。
這種情況是亂碼產生的主要原因。
這種情況其實很常見,電腦程式為了便於統一處理,經常會將所有編碼轉換為一種方式,比如UTF-8, 在轉換的時候,需要知道原來的編碼是什麼,但可能會搞錯,而一旦搞錯,併進行了轉換,就會出現這種亂碼。
這種情況下,無論怎麼切換查看編碼方式,都是不行的。
那有沒有辦法恢復呢?如果有,怎麼恢復呢?
----------------
未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),深入淺出,老馬和你一起探索Java編程及電腦技術的本質。原創文章,保留所有版權。