浅聊字符编码与乱码的那些事

我们总是会碰到乱码的问题,使用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如下表表示(十六进制)。

D6BEC7BF
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编号之间的对应关系,这个映射表是一个简化的说法,实际上可能是一个映射或转换方法。

编码方式十六进制编码编码方式十六进制编码
GB18030C7BFUTF-8E5BCBA
Unicode5F3AUTF-16 LE3A5F

编码转换的具体过程可以是:一个字符从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 A7GB18030脟驴赂莽
UTF-8Ç¿¸çBig5?聶繡癟
Windows-1252Ç¿¸ç

虽然有这么多形式,但我们看到的乱码形式很可能是“Ç¿¸ç”,因为在例子中UTF-8是编码转换的目标编码格式,既然转换为了UTF-8,一般也是要按UTF-8查看。

五、乱码的恢复 

“乱”主要是因为发生了一次错误的编码转换,所谓恢复,是指要恢复两个关键信息:一个是原来的二进制编码方式A;另一个是错误解读的编码方式B。恢复的基本思路是尝试进行逆向操作,假定按一种编码转换方式B获取乱码的二进制格式,然后再假定一种编码解读方式A解读这个二进制,查看其看上去的形式,这要尝试多种编码,如果能找到看着正常的字符形式,应该就可以恢复。

当然了,也不是所有的乱码形式都是可以恢复的,如果形式中有很多不能识别的字符(如?),则很难恢复。另外,如果乱码是由于进行了多次解析和转换错误造成的,也很难恢复。

// 参考书籍:《Java编程逻辑》《Java核心技术》等。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值