@wxf
2017-04-30T11:51:14.000000Z
字数 3640
阅读 2125
老马说编程
我们在处理文件、浏览网页、编写程序时,时不时会碰到乱码的情况。乱码几乎总是令人心烦,让人困惑。希望通过本节和下节文章,你可以自信从容地面对乱码,恢复乱码。
谈乱码,我们就要谈数据的二进制表示,我们已经在前两节谈过整数和小数的二进制表示,接下来我们将讨论字符和文本的二进制表示。编码和乱码听起来比较复杂,内容比较多。所以我们将分为两节来介绍。本节主要介绍各种编码,乱码产生的原因,以及简单乱码的恢复。下节介绍复杂乱码的恢复,以及Java中对字符和文本的处理。
ASCII码作为最早的编码,它用7个二进制位来表示128个字符,即ASCII码中将最高位设为0,用剩下的7位表示字符。ASCII码规定了从0到127中的每个数字代表的含义。如下图:
然而,这些编码并不能满足所有国家的需要,于是,各个国家就发明了各种各样的编码以表示自己国家的字符,为了保持与ASCII码的兼容性,一般都是将最高位设置为1。
在这些扩展的编码中,比较流行有ISO 8859-1、GB2312、GBK和Big5等,我们主要来看一下这些编码。(还有其他编码,在这里不在一一列举)
作用:表示英语及西欧语言
位数:ASCII是用7位表示的,能表示128个字符;其扩展使用8位表示,能表示256个字符
范围:ASCII从0x00到0x7F,扩展从0x00到0xFF
ISO 8859-1属于单字节编码,使用一个字节表示一个字符,最多能够表示的字符范围是0-255,其中0到127与ASCII一样。
作用:扩展ASCII,表示西欧、希腊语等
位数:8位
范围:从0x00到0xFF,兼容ASCII字符集
英文和西欧字符用一个字节就够了,但是中文显然是不够的。于是就出现了能够表示中文的GB2312标准。该标准主要是针对常见的简体中文,包括约7000个汉字。GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是ASCII码。
作用:国家简体中文字符集,兼容ASCII
位数:双字节编码,能表示7445个字符,包括6763个汉字
范围:高字节从0xA1到0xF7,低字节从0xA1到0xFE
GBK建立在GB2312的基础上,向下兼容GB2312,也就是说。GB2312编码的字符和二进制表示,在GBK编码里是完全一样的。
作用:它是GB2312的扩展,加入对繁体字的支持,兼容GB2312
位数:使用两个字节表示,可以表示21886个字符
范围:高字节从0x81到0xFE,低字节从0x40到0x7E和0x80到0xFE
Big5是针对繁体中文的,广泛用于台湾香港等地。一个字符同样固定使用两个字节表示。在这两个字节中,高位字节范围是0x81到0xFE,低位字节范围是0x40到0x7E和0xA1到0xFE。
作用:针对繁体中文的,广泛用于台湾香港等地
位数:使用两个字节表示
范围:高字节从0x81到0xFE,低字节从0x40到0x7E和0xA1到0xFE
之所以出现乱码,是因为看待或者说解析数据的方式错了。纠正的方法只能是使用正确的编码方式进行解读。注意,切换查看编码的方式,并没有改变数据的二进制本身,而只是改变了解析数据的方式,从而改变了数据看起来的样子。很多时候,切换一下编码的查看方式,就可以解决乱码问题。但有时,这样是不够的,我们后面介绍。
上面介绍了中文和西欧的字符和编码,但是世界上还有很多别的国家的字符。这样就造成出现了太多的编码,而且互不兼容。我们能不能对世界上所有的字符统一编码呢?当然可以,这就是Unicode。
Unicode做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000到0x10FFFF。每个字符都有一个Unicode编号,这个编号一般写成16进制,在前面加U+。大部分中文的编号范围在U+4E00到U+9FA5,例如:“马”的Unicode是9A6C。Unicode虽然给所有字符分配了唯一数字编号。但是并没有规定这个编号怎么对应到二进制表示。
那编号怎么对应到二进制表示呢?有多种方案,主要有UTF-32,UTF-16,UTF-8。
这个最简单,就是字符编号的整数二进制形式,使用4个字节表示,非常浪费空间,实际使用比较少。
UTF-16使用变长字节表示。
对编号在U+0000到U+FFFF的字符(常用字符集),直接用两个字节表示。
字符值在U+10000到U+10FFFF之间的字符,使用四个字节表示。
UTF-8使用变长字节表示,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数从1到4个不等。
具体来说,各个Unicode编号范围对应的二进制格式如下所示:
编号范围 | 二进制格式 |
---|---|
0x00-0x7F(0-127) | 0xxxxxxx |
0x80-0x7FF(128-2047) | 110xxxxx 10xxxxxx |
0x800-0xFFFF(2048-65535) | 1110xxx 10xxxxxx 10xxxxxx |
0x10000-0x10FFFF(65536以上) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
上表中的x表示可以用的二进制位,而每个字节开头的1或0是固定的。
编号范围在0-127的编码与ASCII码一样,最高位为0,其他编号的第一个字节有特殊含义,最高位有几个连续的1表示一共用几个字节表示,而其他字节都以10开头。
对于一个Unicode编号,具体怎么编码呢?
首先将Unicode编号看做整数,转化为二进制形式(去掉最高位的0),然后将二进制位从右向左依次填入到对应的二进制格式中,填完后,如果对应的二进制格式还有没填的x,则设为0。
例如:马的Unicode编号是:0x9A6C,整数编号是39532,其对应的UTF-8二进制格式是:1110xxxx 10xxxxxx 10xxxxxx。整数编号39532的二进制格式是1001 101001 101100。将这个二进制位从右到左依次填入二进制格式中,结果就是其UTF-8编码:11101001 10101001 10101100,使用16进制表示为:0xE9A9AC。
UTF-8是兼容ASCII的
对于大部分中文而言,一个中文字符需要用三个字节表示
有了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,然后查Unicode编号->UTF-8编号表,得到其UTF-8编码:E9 A9 AC。
与前文提到的切换查看编码方式正好相反,编码转换改变了数据的二进制格式,但是并没有改变字符看上去的样子。
在前文中,我们提到乱码出现的一个重要原因是解析二进制的方式不对,通过切换查看编码的方式就可以解决乱码。
但如果怎么改变查看方式都不对的话,那很可能就不仅仅是解析二进制的方式不对,而是文本在错误解析的基础上还进行了编码转换。
我们举个例子来说明:
1. 两个字“老马”,本来的编码格式是GB18030,编码是(16进制):C0 CF C2 ED。
2. 这个二进制形式被错误当成了Windows-1252编码,解读成了字符"ÀÏÂí"。
3. 随后这个字符进行了编码转换,转换成了UTF-8编码,形式还是"ÀÏÂí",但二进制变成了:C3 80 C3 8F C3 82 C3 AD,每个字符连个字节。
4. 这个时候再按照GB18030解析,字符就变成了乱码形式"脌脧脗铆",而且这时无论怎么切换查看编码的方式,这个二进制看起来都是乱码。
出现错误编码转换的情况很常见,计算机程序为了便于统一处理,经常会将所有编码转换为一种方式,比如UTF-8,在转换的时候,需要知道原来的编码是什么,但可能会搞错,而一旦搞错,并进行了转换,就会出现这个乱码。这种情况下,无论怎么切换查看编码方式,都是不行的。
那针对上面的问题有没有办法恢复呢?如果有,怎么恢复呢?
参考资料: