本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
亂碼
上節說到亂碼出現的主要原因,即在進行編碼轉換的時候,如果將原來的編碼識別錯了,並進行了轉換,就會發生亂碼,而且這時候無論怎麼切換檢視編碼的方式,都是不行的。
我們來看一個這種錯誤轉換後的亂碼,還是用上節的例子,二進位制是(16進製表示):C3 80 C3 8F C3 82 C3 AD,無論按哪種編碼解析看上去都是亂碼:
UTF-8 | ÀÏÂí |
Windows-1252 | ÀÃÂà |
GB18030 | 脌脧脗鉚 |
Big5 | ���穩 |
雖然有這麼多形式,但我們看到的亂碼形式很可能是"ÀÏÂí",因為在例子中UTF-8是編碼轉換的目標編碼格式,既然轉換為了UTF-8,一般也是要按UTF-8檢視。
亂碼恢復
"亂"主要是因為發生了一次錯誤的編碼轉換,恢復是要恢復兩個關鍵資訊,一個是原來的二進位制編碼方式A,另一個是錯誤解讀的編碼方式B。
恢復的基本思路是嘗試進行逆向操作,假定按一種編碼轉換方式B獲取亂碼的二進位制格式,然後再假定一種編碼解讀方式A解讀這個二進位制,檢視其看上去的形式,這個要嘗試多種編碼,如果能找到看著正常的字元形式,那應該就可以恢復。
這個聽上去可能比較模糊,我們舉個例子來說明,假定亂碼形式是"ÀÏÂí",嘗試多種B和A來看字元形式。我們先使用編輯器,以UltraEdit為例,然後使用Java程式設計來看。
使用UltraEdit
UltraEdit支援編碼轉換和切換檢視編碼方式,也支援檔案的二進位制顯示和編輯,所以我們以UltraEdit為例,其他一些編輯器可能也有類似功能。
新建一個UTF-8編碼的檔案,拷貝"ÀÏÂí"到檔案中。使用編碼轉換,轉換到windows-1252編碼,功能在 "檔案"->"轉換到"->"西歐"->WIN-1252。 轉換完後,開啟十六進位制編輯,檢視其二進位制形式,如下圖所示:
可以看出,其形式還是ÀÏÂí,但二進位制格式變成了 C0 CF C2 ED。這個過程,相當於假設B是windows-1252。這個時候,再按照多種編碼格式檢視這個二進位制,在UltraEdit中,關閉十六進位制編輯,切換檢視編碼方式為GB18030,功能在 "檢視"->"檢視方式(檔案編碼)"->"東亞語言"->GB18030,切換完後,同樣的二進位制神奇的變為了正確的字元形式 "老馬",開啟十六進位制編輯器,可以看出,二進位制還是C0 CF C2 ED,這個GB18030相當於假設A是GB18030。
這個例子我們碰巧第一次就猜對了。實際中,我們可能要做多次嘗試,過程是類似的,先進行編碼轉換(使用B編碼),然後使用不同編碼方式檢視(使用A編碼),如果能找到看上去對的形式,就恢復了。下圖列出了主要的B編碼格式,對應的二進位制,按A編碼解讀的各種形式。
可以看出,第一行是正確的,也就是說原來的編碼其實是A即GB18030,但被錯誤解讀成了B即Windows-1252了。使用Java
關於使用Java我們還有很多知識沒有介紹,但一些讀者已經有很好的Java知識,所以本文一併列出相關程式碼。
Java中處理字串的類有String,String中有我們需要的兩個重要方法:
- public byte[] getBytes(String charsetName),這個方法可以獲取一個字串的給定編碼格式的二進位制形式
- public String(byte bytes[], String charsetName),這個構造方法以給定的二進位制陣列bytes按照編碼格式charsetName解讀為一個字串。
將A看做GB18030,B看做Windows-1252,進行恢復的Java程式碼如下所示:
String str = "ÀÏÂí";
String newStr = new String(str.getBytes("windows-1252"),"GB18030");
System.out.println(newStr);
複製程式碼
先按照B編碼(windows-1252)獲取字串的二進位制,然後按A編碼(GB18030)解讀這個二進位制,得到一個新的字串,然後輸出這個字串的形式,輸出為"老馬"。
同樣,這個一次碰巧就對了,實際中,我們可以寫一個迴圈,測試不同的A/B編碼中的結果形式,程式碼如下所示:
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)假設是: "+charsets[j]+", 被錯誤解讀為了(B): "+charsets[i]);
System.out.println(s);
System.out.println();
}
}
}
}
複製程式碼
以上程式碼使用不同的編碼格式進行測試,如果輸出有正確的,那麼就可以恢復。
恢復的討論
可以看出,這種嘗試需要進行很多次,上面例子嘗試了常見編碼GB18030/Windows 1252/Big5/UTF-8共十二種組合。這四種編碼是常見編碼,在大部分實際應用中應該夠了,但如果你的情況有其他編碼,可以增加一些嘗試。
不是所有的亂碼形式都是可以恢復的,如果形式中有很多不能識別的字元如�?,則很難恢復,另外,如果亂碼是由於進行了多次解析和轉換錯誤造成的,也很難恢復。
小結
上節和本節介紹了編碼的知識,亂碼的原因及恢復方法,這些都是與語言無關的。
接下來,是時候看看在Java中如何表示和處理字元了,我們知道Java中用char型別表示一個字元,但在第三節我們提到了一個問題,即"字元型別怎麼也可以進行算術運算和比較?"。
我們需要對Java中的字元型別有一個更為清晰和深刻的理解。
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。原創文章,保留所有版權。