分析乱码产生的原因及常见乱码的解决方法


在了解怎么解决乱码之前,很有必要了解几种编码格式。

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

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