主要有以下几个原因
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,我们逐个来看下这些编码。
128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1 ~ ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,应用最广泛。
ISO-8859-1 仍然是单字节编码。它向下兼容ASCII,它总共能表示 256 个字符。
具体能表示的字符可见 ISO-8859-1字符
ISO 8859-1虽然号称是标准,用于西欧国家,但它连欧元(€) 这个符号都没有,因为欧元比较晚,而标准比较早。实际使用中更为广泛的是Windows-1252编码,这个编码与ISO8859-1基本是一样的,区别 只在于数字128到159,Windows-1252使用其中的一些数字表示可打印字符。
这个编码中加入了欧元符号以及一些其他常用的字符。基本上可以认为,ISO-8859-1已被Windows-1252取代,在很多应用程序中,即使文件声明它采用的是ISO-8859-1编码,解析的时候依然被当做Windows-1252编码。
它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。
是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字, 兼容GB2312 ,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。
GB18030向下兼容GBK,增加了五万五千多个字符,共七万六千多个字符。包括了很多少数民族字符,以及中日韩统一字符。
用两个字节已经表示不了GB18030中的所有字符,GB18030使用变长编码,有的字符是两个字节,有的是四个字节。
在两字节编码中,字节表示范围与GBK一样。在四字节编码中,第一个字节的值从0x81到0xFE,第二个字节的值从0x30到0x39,第三个字节的值从0x81到0xFE,第四个字节的值从0x30到0x39。
解析二进制时,如何知道是两个字节还是四个字节表示一个字符呢?看第二个字节的范围,如果是0x30到0x39就是四个字节表示,因为两个字节编码中第二字节都比这个大。
Big5是针对繁体中文的,广泛用于台湾香港等地。
Big5包括1万3千多个繁体字,和GB2312类似,一个字符同样固定使用两个字节表示。在这两个字节中,高位字节范围是0x81-0xFE,低位字节范围是0x40-0x7E和0xA1-0xFE。
以上我们介绍了中文和西欧的字符与编码,但世界上还有很多别的国家的字符,每个国家的各种计算机厂商都对自己常用的字符进行编码,在编码的时候基本忽略了别的国家的字符和编码,甚至忽略了同一国家的其他计算机厂商,这样造成的结果就是,出现了太多的编码,且互相不兼容。
世界上所有的字符能不能统一编码呢?可以,这就是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。
这个最简单,就是字符编号的整数二进制形式,四个字节。
但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian, BE),否则,正好相反的情况,就叫“小端”(Little Endian, LE)。对应的编码方式分别是UTF-32BE和UTF-32LE。
可以看出,每个字符都用四个字节表示,非常浪费空间,实际采用的也比较少。
有了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。
与前文提到的切换查看编码方式正好相反,编码转换改变了数据的二进制格式,但并没有改变字符看上去的样子。
首先需要明白的一点是,在计算机并不会真正保存我们输入的汉字的,在计算机中会以字节码的方式保存,再以指定编码输出才是我们所看到的。现在我们来分析一下乱码产生的原因:
以下图示例来说明
上图是以字符串“中文字符+English”为例,在原系统采用utf-8编码的情况下生成了字节码A(具体是多少不重要),再对字节码A采用ASCII解码,就会产生乱码。因为ASCII码的字符集少于UTF-8的字符集,UTF-8的部分字符集不能被ASCII识别,所以会产生乱码。
所以产生乱码的主要原因是文件的编码格式与解码格式不一致或不兼容导致的。知道了原因那怎么解决乱码呢?首先要明白不是所有乱码都是可以恢复的。
在上图中,经ASCII解码后的乱码就已经损失了原字节码值,毕竟UTF-8和ASCII的编码范围就有很大区别。
如果我们知道文件的编码格式,那么按照编码格式进行解码即可解决。但是很多时候,我们并不知道,可用java程序实现用不同编码测试来恢复。也看使用Notepad++自带的编码格式通过切换使用不同的编码进行尝试来恢复乱码(注意:在切换使用编码时,另备份文件,编码过程是不可逆的)。
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