分析亂碼產生的原因及常見亂碼的解決方法


在瞭解怎麼解決亂碼之前,很有必要了解幾種編碼格式。

1、爲什麼需要編碼

主要有以下幾個原因

  1. 計算機中存儲信息的最小單元是一個字節即 8 個 bit,所以能表示的字符範圍是 0~255 ;
  2. 人類要表示的符號太多,無法用一個字節來完全表示;
  3. 要解決這個問題,就需要編碼

2、常見編碼

ASCII

ASCII(American Standard Code for Information Interchange,美國信息互換標準代碼),是現今最通用的單字節編碼系統。

ASCII 的高位是0,低 7 位表示具體字符。共有 128個字符,0-31、127 是控制字符如換行回車刪除等;32~126 是特殊字符、字母、數字等。

具體能表示的字符可見 ASCII碼對照表

爲了保持與ASCII 碼的兼容性,一般都是將最高位設置爲1。也就是說,當最高位爲0時,表示ASCII 碼,當爲1時就是各個國家自己的字符。

在這些擴展的編碼中,在西歐國家中流行的是ISO-8859-1和Windows-1252,在中國是GB2312,GBK,GB18030和Big5,我們逐個來看下這些編碼。

ISO-8859-1(擴展ASCII編碼)

128 個字符顯然是不夠用的,於是 ISO 組織在 ASCII 碼基礎上又制定了一些列標準用來擴展 ASCII 編碼,它們是 ISO-8859-1 ~ ISO-8859-15,其中 ISO-8859-1 涵蓋了大多數西歐語言字符,應用最廣泛。

ISO-8859-1 仍然是單字節編碼。它向下兼容ASCII,它總共能表示 256 個字符。

具體能表示的字符可見 ISO-8859-1字符

Windows-1252

ISO 8859-1雖然號稱是標準,用於西歐國家,但它連歐元(€) 這個符號都沒有,因爲歐元比較晚,而標準比較早。實際使用中更爲廣泛的是Windows-1252編碼,這個編碼與ISO8859-1基本是一樣的,區別 只在於數字128到159,Windows-1252使用其中的一些數字表示可打印字符。

這個編碼中加入了歐元符號以及一些其他常用的字符。基本上可以認爲,ISO-8859-1已被Windows-1252取代,在很多應用程序中,即使文件聲明它採用的是ISO-8859-1編碼,解析的時候依然被當做Windows-1252編碼。

GB2312

它是雙字節編碼,總的編碼範圍是 A1-F7,其中從 A1-A9 是符號區,總共包含 682 個符號,從 B0-F7 是漢字區,包含 6763 個漢字。

GBK(擴展GB2312)

是爲了擴展 GB2312,加入更多的漢字,它的編碼範圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示 21003 個漢字, 兼容GB2312 ,也就是說用 GB2312 編碼的漢字可以用 GBK 來解碼,並且不會有亂碼。

GB18030(兼容GB2312,GBK)

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。

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 具體定義了 Unicode 字符在計算機中存取方法。
  • UTF-16 用兩個字節來表示 Unicode 轉化格式,不論什麼字符都可以用兩個字節表示,兩個字節是 16 個 bit,所以叫 UTF-16。
  • UTF-16 表示字符非常方便,每兩個字節表示一個字符,這個在字符串操作時就大大簡化了操作,這也是 Java 以 UTF-16 作爲內存的字符存儲格式的一個很重要的原因。
  • 從 UTF-16 編碼規則來看,僅僅將字符的高位和低位進行拆分變成兩個字節。特點是編碼效率非常高,規則很簡單,由於不同處理器對 2 字節處理方式不同,Big-endian(高位字節在前,低位字節在後)或 Little-endian(低位字節在前,高位字節在後)編碼。如果高位存放在前面就叫大端(BE),編碼就叫UTF-16BE,否則就叫小端,編碼就叫UTF-16LE。

UTF-8

  • UTF-16 統一採用兩個字節表示一個字符,雖然在表示上非常簡單方便,但是也有其缺點,有很大一部分字符用一個字節就可以表示的現在要兩個字節表示,存儲空間放大了一倍,在現在的網絡帶寬還非常有限的今天,這樣會增大網絡傳輸的流量。
  • UTF-8 採用了一種變長技術,每個編碼區域有不同的編碼長度。不同類型的字符可以是由 1~6 個字節組成。
  • UTF-8 有以下編碼規則:
    • 如果一個字節最高位(第 8 位)爲 0,表示這是一個 ASCII 字符(00 - 7F)。可見,所有 ASCII 編碼已經是 UTF-8 了。
    • 如果一個字節以 11 開頭,連續的 1 的個數暗示這個字符的字節數,例如:110xxxxx 代表它是雙字節 UTF-8 字符的首字節。
    • 如果一個字節,以 10 開始,表示它不是首字節,需要向前查找才能得到當前字符的首字節 。

幾種編碼的比較

  • UTF-16 編碼效率最高,字符到字節相互轉換更簡單,進行字符串操作也更好。
  • 它適合在本地磁盤和內存之間使用,可以進行字符和字節之間快速切換,如 Java 的內存編碼就是採用 UTF-16 編碼。
  • 但是它不適合在網絡之間傳輸,因爲網絡傳輸容易損壞字節流,一旦字節流損壞將很難恢復。
  • 相比較而言 UTF-8 更適合網絡傳輸,對 ASCII 字符采用單字節存儲,另外單個字符損壞也不會影響後面其它字符,在編碼效率上介於 GBK 和 UTF-16 之間。
  • 所以 UTF-8 在編碼效率上和編碼安全性上做了平衡,是理想的中文編碼方式。

編碼轉換

有了Unicode之後,每一個字符就有了多種不兼容的編碼方式,比如說"馬"這個字符,它的各種編碼方式對應的16進制是:

編碼格式 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。

與前文提到的切換查看編碼方式正好相反,編碼轉換改變了數據的二進制格式,但並沒有改變字符看上去的樣子。

3. 亂碼產生的原因

首先需要明白的一點是,在計算機並不會真正保存我們輸入的漢字的,在計算機中會以字節碼的方式保存,再以指定編碼輸出纔是我們所看到的。現在我們來分析一下亂碼產生的原因:

以下圖示例來說明

UTF-8編碼
中文字符+English
字節碼A
ASCII解碼
字節碼A
亂碼

上圖是以字符串“中文字符+English”爲例,在原系統採用utf-8編碼的情況下生成了字節碼A(具體是多少不重要),再對字節碼A採用ASCII解碼,就會產生亂碼。因爲ASCII碼的字符集少於UTF-8的字符集,UTF-8的部分字符集不能被ASCII識別,所以會產生亂碼。

所以產生亂碼的主要原因是文件的編碼格式與解碼格式不一致或不兼容導致的。知道了原因那怎麼解決亂碼呢?首先要明白不是所有亂碼都是可以恢復的。

在上圖中,經ASCII解碼後的亂碼就已經損失了原字節碼值,畢竟UTF-8和ASCII的編碼範圍就有很大區別。

4. 嘗試恢復亂碼

如果我們知道文件的編碼格式,那麼按照編碼格式進行解碼即可解決。但是很多時候,我們並不知道,可用java程序實現用不同編碼嘗試

public static void recover(String str) 
        throws UnsupportedEncodingException{
    String[] charsets = new String[]{"windows-1252","GB18030","Big5","UTF-8","ISO-8859-1"};
    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);
           
            }
        }
    }
}

如文中有任何問題,歡迎指正!

參考鏈接:
https://www.cnblogs.com/maohuidong/p/8044568.html
https://www.cnblogs.com/swiftma/p/5420145.html

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