我们总是会碰到乱码的问题,使用tomcat时控制台中文乱码,使用servlet时,传递中文到网页时会乱码,甚至使用wps编辑的文件,使用word打开时也会出现乱码···我们总是会碰到乱码,乱码让人心烦,让人无奈。阅读完本文,我们就可以轻松的面对乱码了。本文会先介绍常见的编码格式,之后讲解编码转换,之后分析乱码出现的原因,最后介绍如何恢复乱码。
一、常见的非Unicode编码
我们注意生活中编码的各种格式,会见到GB2312、GBK、ASCII、ISO-8859、Windows-1252和Big5等。
1.ASCII
ASCII(American Standard Code for Information Interchange)是最早的字符编码标准,最初由美国国家标准协会(ANSI)于1963年发布。它将高位设置为0,使用7位(7-bit)二进制数表示128个字符,包括英文字母、数字、标点符号和一些控制字符。
ASCII编码表对应了0到127的字符范围,其中包括以下内容:
- 控制字符(0~31):这些字符被用来控制设备或表示一些特殊功能,如回车、换行、制表符等。
- 可打印字符(32~126):这些字符是可以直接显示在屏幕上的可打印字符,包括英文字母(大小写)、数字、标点符号和一些常见特殊字符。
- 删除字符(127):表示删除键。
ASCII编码是基于单字节的编码,每个字符仅使用一个字节(8位)来表示。这128个字符对于美国来说是够了的,但对于上下5000年文化的中国来说是不够的,于是在国内出现了GB2312、GBK、GB18030和Big5等,西欧国家中流行的是ISO 8859-1和Windows-1252。
2.ISO-8859
ISO-8859系列定义了多种字符集,每个字符集都以ISO-8859-前缀命名,后面跟着一个数字,表示具体的字符编码。目前最常用的ISO-8859字符集包括:
1)ISO-8859-1(Latin-1):也称为欧洲字符集,覆盖了大部分欧洲语言,包括英语、法语、德语、西班牙语等。它使用8位(8-bit)编码,扩展了ASCII编码范围,共支持256个字符。
2)ISO-8859-2(Latin-2):也称为中欧字符集,覆盖了中欧和东欧地区的多种语言,如捷克语、波兰语、匈牙利语等。
3)ISO-8859-5(Latin/Cyrillic):覆盖了使用西里尔字母的斯拉夫语族语言,如俄语、乌克兰语、保加利亚语等。
4)ISO-8859-15(Latin-9):是ISO-8859-1的改进版本,增加了一些特殊字符,如欧元符号。它主要用于欧洲各国的文本处理。
ISO-8859也是使用一个字节表示一个字符,其中0~127与ASCII一样,128~255规定了不同的含义。
3.Windows-1252
Windows-1252是一种字符编码,也被称为Windows西欧字符集(Windows Western Europe character set)。它是一种扩展ASCII编码,由微软公司在Windows操作系统中广泛使用。
Windows-1252编码包含了ASCII编码的所有字符,以及128个额外的字符,用于支持西欧语言,如英语、法语、德语、西班牙语等。这些额外的字符包括特殊符号、货币符号、重音字母和其他特殊字符。
与标准的ISO-8859-1字符集相比,Windows-1252在位置0x80到0x9F之间有一些差异。在Windows-1252中,这些位置上的字符被用于表示一些特殊符号和字母,而不同于ISO-8859-1中的字符。例如,Windows-1252中的位置0x80表示欧元符号(€),而ISO-8859-1中则没有定义该字符。
HTML5甚至明确规定,如果文件声明的是ISO 8859-1编码,它应该被看作Win-dows-1252编码。为什么要这样呢?因为大部分人搞不清楚ISO 8859-1和Windows-1252的区别,当他说ISO 8859-1的时候,其实他指的是Windows-1252,所以标准干脆就这么强制规定了。
4.GB2312
美国和西欧字符用一个字节就够了,但中文显然是不够的。中文第一个标准是GB2312。GB2312标准主要针对的是简体中文常见字符,包括约7000个汉字和一些罕用词和繁体字。GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是ASCII字符。在这两个字节中,其中高位字节范围是0xA1~0xF7,低位字节范围是0xA1~0xFE。例如:“强哥”的GB2312如下表表示(十六进制)。
强 | 哥 |
D6BE | C7BF |
5.GBK
GBK建立在GB2312的基础上,向下兼容GB2312,也就是说,GB2312编码的字符和二进制表示,在GBK编码里是完全一样的。GBK增加了14 000多个汉字,共计约21 000个汉字,其中包括繁体字。GBK同样使用固定的两个字节表示,其中高位字节范围是0x81~0xFE,低位字节范围是0x40~0x7E和0x80~0xFE。
6.Big5
Big5是针对繁体中文的,广泛用于我国台湾地区和我国香港特别行政区等地。Big5包括13 000多个繁体字,和GB2312类似,一个字符同样固定使用两个字节表示。在这两个字节中,高位字节范围是0x81~0xFE,低位字节范围是0x40~0x7E和0xA1~0xFE。
二、Unicode编码
Unicode 做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000~0x10FFFF,包括110多万。但大部分常用字符都在0x0000~0xFFFF之间,即65 536个数字之内。每个字符都有一个Unicode编号,这个编号一般写成十六进制,在前面加U+。大部分中文的编号范围为U+4E00~U+9FFF,例如,“强”的Unicode是U+5F3A。编号可通过UTF-32、UTF-16和UTF-8方式对应到二进制。
1.UTF-32
这个最简单,就是字符编号的整数二进制形式,4个字节。但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“大端”(Big Endian,BE),否则,就叫“小端”(Little Endian, LE)。对应的编码方式分别是UTF-32BE和UTF-32LE。可以看出,每个字符都用4个字节表示,非常浪费空间,实际采用的也比较少。
2.UTF-16
1)对于编号在U+0000~U+FFFF的字符(常用字符集),直接用两个字节表示。需要说明的是,U+D800~U+DBFF的编号其实是没有定义的。
2)字符值在U+10000~U+10FFFF的字符(也叫做增补字符集),需要用4个字节表示。前两个字节叫高代理项,范围是U+D800~U+DBFF;后两个字节叫低代理项,范围是U+DC00~U+DFFF。
区分是两个字节还是4个字节表示一个字符就看前两个字节的编号范围,如果是U+D800~U+DBFF,就是4个字节,否则就是两个字节。UTF-16也有和UTF-32一样的字节序问题,如果高位存放在前面就叫大端(BE),编码就叫UTF-16BE,否则就叫小端,编码就叫UTF-16LE。UTF-16常用于系统内部编码,UTF-16比UTF-32节省了很多空间,但在存储空间上比UTF-8编码更为浪费,因为它使用固定的两个字节来表示每个字符。但与UTF-8相比,UTF-16可以更快地进行索引和随机访问。
3.UTF-8
UTF-8使用变长字节表示,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为1~4不等。具体来说,各个Unicode编号范围对应的二进制格式如下图所示。
上图中的x表示可以用的二进制位,而每个字节开头的1或0是固定的。小于128的,编码与ASCII码一样,最高位为0。其他编号的第一个字节有特殊含义,最高位有几个连续的1就表示用几个字节表示,而其他字节都以10开头。对于一个Unicode编号,具体怎么编码呢?首先将其看作整数,转化为二进制形式(去掉高位的0),然后将二进制位从右向左依次填入对应的二进制格式x中,填完后,如果对应的二进制格式还有没填的x,则设为0。我们来看个例子,“强”的Unicode编号是0x5F3A,整数编号是24 378,其对应的UTF-8二进制格式是:
1110xxxx 10xxxxxx 10xxxxxx
整数编号24 378的二进制格式是:
0101 111100 111010
整数编号24 378的二进制格式是:
11100101 10111100 10111010
十六进制表示为0xE5BCBA。
和UTF-32/UTF-16不同,UTF-8是兼容ASCII的,对大部分中文而言,一个中文字符需要用三个字节表示。
三、编码转换
有了Unicode之后,每一个字符就有了多种不兼容的编码方式,比如说“强”这个字符,它的各种编码方式对应的十六进制如下表所示。这几种格式之间可以借助Unicode编号进行编码转换。可以认为:每种编码都有一个映射表,存储其特有的字符编码和Unicode编号之间的对应关系,这个映射表是一个简化的说法,实际上可能是一个映射或转换方法。
编码方式 | 十六进制编码 | 编码方式 | 十六进制编码 |
GB18030 | C7BF | UTF-8 | E5BCBA |
Unicode | 5F3A | UTF-16 LE | 3A5F |
编码转换的具体过程可以是:一个字符从A编码转到B编码,先找到字符的A编码格式,通过A的映射表找到其Unicode编号,然后通过Unicode编号再查B的映射表,找到字符的B编码格式。举例来说,“强”从GB18030转到UTF-8,先查GB18030->Unicode编号表,得到其编号是C7 BF,然后查Uncode编号->UTF-8表,得到其UTF-8编码:E5 BCBA。编码转换改变了字符的二进制内容,但并没有改变字符看上去的样子。
四、乱码的原因
理解了编码,我们来看乱码。乱码有两种常见原因:一种比较简单,就是简单的解析错误;另外一种比较复杂,在错误解析的基础上进行了编码转换。我们分别介绍。
1.解析错误
看个简单的例子。一个法国人采用Windows-1252编码写了个文件,发送给了一个中国人,中国人使用GB18030来解析这个字符,看到的可能就是乱码。比如,法国人发送的是Pékin, Windows-1252的二进制(采用十六进制)是50 E9 6B 69 6E,第二个字节E9对应é,其他都是ASCII码,中国人收到的也是这个二进制,但是他把它看成了GB18030编码,GB18030中E9 6B对应的是字符“閗”,于是他看到的就是“P閗in”,这看来就是一个乱码。反之也是一样的,一个GB18030编码的文件如果被看作Windows-1252也是乱码。
这种情况下,之所以看起来是乱码,是因为看待或者说解析数据的方式错了。只要使用正确的编码方式进行解读就可以纠正了。很多文件编辑器,如NotePad++、记事本都有切换查看编码方式的功能,浏览器也都有切换查看编码方式的功能,如Fire-fox,在菜单“查看”→“文字编码”中即可找到该功能。切换查看编码的方式并没有改变数据的二进制本身,而只是改变了解析数据的方式,从而改变了数据看起来的样子,这与前面提到的编码转换正好相反。很多时候,做这样一个编码查看方式的切换就可以解决乱码的问题,但有的时候这样是不够的。
2.错误的解析和编码转换
如果怎么改变查看方式都不对,那很有可能就不仅仅是解析二进制的方式不对,而是文本在错误解析的基础上还进行了编码转换。我们举个例子来说明:
1)两个字“强哥”,本来的编码格式是GB18030,编码(十六进制)是C7 BF B8 E7。
2)这个二进制形式被错误当成了Windows-1252编码,解读成了字符“Ç¿¸ç”。
3)随后这个字符进行了编码转换,转换成了UTF-8编码,形式还是“Ç¿¸ç”,但二进制变成了C3 87 C2 BF C2 B8 C3 A7,每个字符两个字节。
4)这个时候再按照GB18030解析,字符就变成了乱码形式“脟驴赂莽”,而且这时无论怎么切换查看编码的方式,这个二进制看起来都是乱码。
这种情况是乱码产生的主要原因。这种情况其实很常见,计算机程序为了便于统一处理,经常会将所有编码转换为一种方式,比如UTF-8,在转换的时候,需要知道原来的编码是什么,但可能会搞错,而一旦搞错并进行了转换,就会出现这种乱码。这种情况下,无论怎么切换查看编码方式都是不行的,如下表所示。
编码方式 | 结果 | 编码方式 | 结果 |
十六进制 | C3 87 C2 BF C2 B8 C3 A7 | GB18030 | 脟驴赂莽 |
UTF-8 | Ç¿¸ç | Big5 | ?聶繡癟 |
Windows-1252 | Ç¿¸ç |
虽然有这么多形式,但我们看到的乱码形式很可能是“Ç¿¸ç”,因为在例子中UTF-8是编码转换的目标编码格式,既然转换为了UTF-8,一般也是要按UTF-8查看。
五、乱码的恢复
“乱”主要是因为发生了一次错误的编码转换,所谓恢复,是指要恢复两个关键信息:一个是原来的二进制编码方式A;另一个是错误解读的编码方式B。恢复的基本思路是尝试进行逆向操作,假定按一种编码转换方式B获取乱码的二进制格式,然后再假定一种编码解读方式A解读这个二进制,查看其看上去的形式,这要尝试多种编码,如果能找到看着正常的字符形式,应该就可以恢复。
当然了,也不是所有的乱码形式都是可以恢复的,如果形式中有很多不能识别的字符(如?),则很难恢复。另外,如果乱码是由于进行了多次解析和转换错误造成的,也很难恢复。
// 参考书籍:《Java编程逻辑》《Java核心技术》等。