亂碼
上節(jié)說到亂碼出現(xiàn)的主要原因,即在進(jìn)行編碼轉(zhuǎn)換的時(shí)候,如果將原來的編碼識(shí)別錯(cuò)了,并進(jìn)行了轉(zhuǎn)換,就會(huì)發(fā)生亂碼,而且這時(shí)候無論怎么切換查看編碼的方式,都是不行的。
我們來看一個(gè)這種錯(cuò)誤轉(zhuǎn)換后的亂碼,還是用上節(jié)的例子,二進(jìn)制是(16進(jìn)制表示):C3 80 C3 8F C3 82 C3 AD,無論按哪種編碼解析看上去都是亂碼:
UTF-8 | à??í |
Windows-1252 | ?€?????- |
GB18030 | 脌脧脗鉚 |
Big5 | ???穩(wěn) |
雖然有這么多形式,但我們看到的亂碼形式很可能是"à??í",因?yàn)樵诶又蠻TF-8是編碼轉(zhuǎn)換的目標(biāo)編碼格式,既然轉(zhuǎn)換為了UTF-8,一般也是要按UTF-8查看。
亂碼恢復(fù)
"亂"主要是因?yàn)榘l(fā)生了一次錯(cuò)誤的編碼轉(zhuǎn)換,恢復(fù)是要恢復(fù)兩個(gè)關(guān)鍵信息,一個(gè)是原來的二進(jìn)制編碼方式A,另一個(gè)是錯(cuò)誤解讀的編碼方式B。
恢復(fù)的基本思路是嘗試進(jìn)行逆向操作,假定按一種編碼轉(zhuǎn)換方式B獲取亂碼的二進(jìn)制格式,然后再假定一種編碼解讀方式A解讀這個(gè)二進(jìn)制,查看其看上去的形式,這個(gè)要嘗試多種編碼,如果能找到看著正常的字符形式,那應(yīng)該就可以恢復(fù)。
這個(gè)聽上去可能比較模糊,我們舉個(gè)例子來說明,假定亂碼形式是"à??í",嘗試多種B和A來看字符形式。我們先使用編輯器,以UltraEdit為例,然后使用Java編程來看。
使用UltraEdit
UltraEdit支持編碼轉(zhuǎn)換和切換查看編碼方式,也支持文件的二進(jìn)制顯示和編輯,所以我們以UltraEdit為例,其他一些編輯器可能也有類似功能。
新建一個(gè)UTF-8編碼的文件,拷貝"à??í"到文件中。使用編碼轉(zhuǎn)換,轉(zhuǎn)換到windows-1252編碼,功能在 "文件"->"轉(zhuǎn)換到"->"西歐"->WIN-1252。
轉(zhuǎn)換完后,打開十六進(jìn)制編輯,查看其二進(jìn)制形式,如下圖所示:
可以看出,其形式還是à??í,但二進(jìn)制格式變成了 C0 CF C2 ED。這個(gè)過程,相當(dāng)于假設(shè)B是windows-1252。這個(gè)時(shí)候,再按照多種編碼格式查看這個(gè)二進(jìn)制,在UltraEdit中,關(guān)閉十六進(jìn)制編輯,切換查看編碼方式為GB18030,功能在 "視圖"->"查看方式(文件編碼)"->"東亞語言"->GB18030,切換完后,同樣的二進(jìn)制神奇的變?yōu)榱苏_的字符形式 "老馬",打開十六進(jìn)制編輯器,可以看出,二進(jìn)制還是C0 CF C2 ED,這個(gè)GB18030相當(dāng)于假設(shè)A是GB18030。
這個(gè)例子我們碰巧第一次就猜對(duì)了。實(shí)際中,我們可能要做多次嘗試,過程是類似的,先進(jìn)行編碼轉(zhuǎn)換(使用B編碼),然后使用不同編碼方式查看(使用A編碼),如果能找到看上去對(duì)的形式,就恢復(fù)了。下圖列出了主要的B編碼格式,對(duì)應(yīng)的二進(jìn)制,按A編碼解讀的各種形式。
可以看出,第一行是正確的,也就是說原來的編碼其實(shí)是A即GB18030,但被錯(cuò)誤解讀成了B即Windows-1252了。
使用Java
關(guān)于使用Java我們還有很多知識(shí)沒有介紹,但一些讀者已經(jīng)有很好的Java知識(shí),所以本文一并列出相關(guān)代碼。
Java中處理字符串的類有String,String中有我們需要的兩個(gè)重要方法:
- public byte[] getBytes(String charsetName),這個(gè)方法可以獲取一個(gè)字符串的給定編碼格式的二進(jìn)制形式
- public String(byte bytes[], String charsetName),這個(gè)構(gòu)造方法以給定的二進(jìn)制數(shù)組bytes按照編碼格式charsetName解讀為一個(gè)字符串。
將A看做GB18030,B看做Windows-1252,進(jìn)行恢復(fù)的Java代碼如下所示:
String str = "à??í";
String newStr = new String(str.getBytes("windows-1252"),"GB18030");
System.out.println(newStr);
先按照B編碼(windows-1252)獲取字符串的二進(jìn)制,然后按A編碼(GB18030)解讀這個(gè)二進(jìn)制,得到一個(gè)新的字符串,然后輸出這個(gè)字符串的形式,輸出為"老馬"。
同樣,這個(gè)一次碰巧就對(duì)了,實(shí)際中,我們可以寫一個(gè)循環(huán),測(cè)試不同的A/B編碼中的結(jié)果形式,代碼如下所示:
public static void recover(String str)
throws UnsupportedEncodingException{
String[] charsets = new String[]{"windows-1252","GB18030","Big5","UTF-8"};
for(int i=0;i<charsets.length;i++){
for(int j=0;j<charsets.length;j++){
if(i!=j){
String s = new String(str.getBytes(charsets[i]),charsets[j]);
System.out.println("---- 原來編碼(A)假設(shè)是: "+charsets[j]+", 被錯(cuò)誤解讀為了(B): "+charsets[i]);
System.out.println(s);
System.out.println();
}
}
}
}
以上代碼使用不同的編碼格式進(jìn)行測(cè)試,如果輸出有正確的,那么就可以恢復(fù)。
恢復(fù)的討論
可以看出,這種嘗試需要進(jìn)行很多次,上面例子嘗試了常見編碼GB18030/Windows 1252/Big5/UTF-8共十二種組合。這四種編碼是常見編碼,在大部分實(shí)際應(yīng)用中應(yīng)該夠了,但如果你的情況有其他編碼,可以增加一些嘗試。
不是所有的亂碼形式都是可以恢復(fù)的,如果形式中有很多不能識(shí)別的字符如??,則很難恢復(fù),另外,如果亂碼是由于進(jìn)行了多次解析和轉(zhuǎn)換錯(cuò)誤造成的,也很難恢復(fù)。
小結(jié)
上節(jié)和本節(jié)介紹了編碼的知識(shí),亂碼的原因及恢復(fù)方法,這些都是與語言無關(guān)的。
接下來,是時(shí)候看看在Java中如何表示和處理字符了,我們知道Java中用char類型表示一個(gè)字符,但在第三節(jié)我們提到了一個(gè)問題,即"字符類型怎么也可以進(jìn)行算術(shù)運(yùn)算和比較?"。
我們需要對(duì)Java中的字符類型有一個(gè)更為清晰和深刻的理解。
未完待續(xù),查看最新文章,敬請(qǐng)關(guān)注微信公眾號(hào)“老馬說編程”(掃描下方二維碼),深入淺出,老馬和你一起探索Java編程及計(jì)算機(jī)技術(shù)的本質(zhì)。原創(chuàng)文章,保留所有版權(quán)。