在我们处理文件或者处理程序字符时,时不时会遇到乱码的情况,而且这些乱码的情况让人很困惑,大多时候都是CV某度一下,看看有没有相关类似情况的博文出现,如果有那就按照博文上的方式一步一步去解决就好,如果没找到,很多人就直接emo了。其实,乱码听起来挺复杂的,其实也不是非常难解决。今天来聊一聊关于如何解决字符的编码和乱码问题。
什么是编码编码是信息从一种形式或格式转换为另一种形式的过程,也称为计算机编程语言的代码简称编码。用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。编码在电子计算机、电视、遥控和通讯等方面广泛使用。编码是信息从一种形式或格式转换为另一种形式的过程。解码,是编码的逆过程。
编码分类在日常的开发中,有非常多的编码的格式,比如GBK、UTF-8、ASCII等等,很多人都不清楚这些编码格式之间有什么区别联系,只知道编码和解码的格式用同一种就不会出现乱码问题。其实在计算机软件领域编码就划分为两大类,一种是Unicode编码,另一种是非Unicode编码。
非Unicode编码常见的非Unicode编码有,ASCII、GBK、GB18030、ISO8859-1、windows-1252 等;
ASCII我们都知道世界第一台计算机的诞生在美国,当时的作者没考虑那么多,只考虑了美国的需求(美国大概使用128个字符),所以规定了当时128个字符的二进制表示的方法,也就是一套标准,即ASCII,全称American Standard Code for Information Interchange,译为美国信息互换标准代码。
计算机存储的最小单位是byte,即8位,128个字符刚好可以使用7位表示,在ASCII中,最高位设置为0,另外7位可以使用0~127来表示字符,其中在ASCII编码中规定了0~127各个数字代表的含义,基本上能覆盖键盘上的所有字符。
需要注意的是,在ASCII中存在少数比较特殊不可打印的字符,常用不可打印的字符有
ASCII码对于美国来说是够用了,但世界上的国家那么多,字符和语言也不一样,于是,各个国家的各种计算机厂商就发明了各种各种的编码方式以表示自己国家的字符,为了保持与ASCII码的兼容性,一般都是将最高位设置为1。也就是说,当最高位为0时,表示的是标准的ASCII码,为1时就是各个国家扩展自己的字符的编码。在这些扩展的编码中,在西欧国家中流行的是ISO 8859-1和Windows-1252,在中国是GB2312、GBK、GB18030,等会挨个介绍这些编码的区别。
ISO 8859-1和Windows-1252ISO 8859-1又称Latin-1,它是使用一个字节表示一个字符,其中0~127与ASCII一样,128~255规定了不同的含义。在128~255中,128~159表示一些控制字符,160~255表示一些西欧字符,这些字符在中国也不常用,就不一一介绍了。
ISO 8859-1虽然号称是用于西欧国家的统一标准,但它连欧元()这个符号都没有,因为欧元这个符号比较晚,而ISO 8859-1标准比较早。所以实际中使用更为广泛的是Windows-1252 编码,这个编码与ISO 8859-1基本是一样的,区别只在干数字128~159。HTML5甚至明确规定,如果文件声明的是ISO 8859-1编码,它应该被也可以看作Windows-1252编码。为什么要这样呢?因为大部分人搞不清楚ISO 8859-1和Windows-1252的区别,当他说ISO 8859-1的时候,其实他指的是Windows-1252,所以标准干脆电脑就这么强制规定了。Windows-1252使用其中的一些数字表示可打印字符
GB2312、GBK、GB18030这三种编码格式相信国内的开发者都不陌生了,这三种也就是中文字符显示的编码格式,那这三种之间有什么区别和联系呢?
GB2312美国和西欧字符用一个字节就够了,但中文显然是不够的。中文第一个标准是GB2312。GB2312标准主要针对的是简体中文常见字符,包括约7000个汉字和一些罕用词和繁体字。
GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是ASCII字符。在这两个字节中,其中高位字节范围是0xA1~0xF7,低位字节范围是0xA1~0xFE。
GBKGBK建立在GB2312的基础上,向下兼容GB2312,也就是说,GB2312编码的字符和二进制表示,在 GBK编码里是完全一样的。GBK增加了14000多个汉字,共计约21000个汉字,其中包括繁体字。
GBK同样体里固定的两个字节表示,其中高位字节范围是0x81~0xFE,低位字节范围是0x40~0x7F和Ox80~0xFE。
需要注意的是,低位字节是从Ox40(也就是64)开始的,也就是说,低位字节的最高位可能为0。那怎么知道它是汉字的一部分,还是一个ASCII字符呢?其实很简单,因为汉字是用固定两个字节表示的.在解析二进制流的时候,如果第一个字节的最高位为1,那么就将下一个字节读进来一起解析为一个汉字,而不用考虑它的最高位,电脑解析完后,跳到第三个字节继续解析。
GB18030GB18030向下兼容GBK,增加了55000多个字符,共76000多个字符,包括了很多少数民族字符,以及中日韩统一字符。
用两个字节已经表示不了GB18030中的所有字符,GB18030使用变长编码,有的字符是两个字节,有的是四个字节。在两字节编码中,字节表示范围与GBK一样。在四字节编码中,第一个字节的值为0x81~0xFE,第二个字节的值为0x30~0x39,第三个字节的值为0x81~0xFE,第四个字节的值为0x30~0x39。
解析二进制时,如何知道是两个字节还是4个字节表示一个字符呢?看第二个字节的范围,如果是0x30~0x39就是4个字节表示,因为两个字节编码中第二个字节都比这个大。
Unicode编码如果说上述的编码能表示中文、英语等所需的字符,那世界那么,国家语言多种多样,每个国家都基于ASCII去实现一套编码标准,那么将会出现成千上万套编码。那么就没有一套世界统一的标准?有,这就是Unicode编码!
Unicode做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000~0x10EEEF,包括110多万。但大部分常用字符都在0x0000~0xEEEF之间,即65536个数字之内。每个字符都有一个Unicode编号,这个编号一般写成十六进制,在前面加U+。大部分中文的编号范围为U+4E00~U+9FFF。
简单理解,Unicode主要做了这么一件事,就是给所有字符分配了唯一数字编号。它并没有规定这个编号怎么对应到二进制表示,这是与上面介绍的其他编码不同的,其他编码都既规定了能表示哪些字符,又规定了每个字符对应的二进制是什么,而Unicode本身只规定了每个字符的数字编号是多少。目前常用的编码方案有UTF-8、UTF-16以及UTF-32。
UTF-8UTF-8使用变长字节表示,每个字符使用的字节个数与其电脑Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数为1~4不等。小于128的,编码与ASCII码一样,最高位为0。其他编号的第一个字节有特殊含义,最高位有几个连续的1就表示用几个字节表示,而其他字节都以10开头。
对于一个Unicode编号,具体怎么编码呢?首先将其看作整数,转化为二进制形式(去掉高位的0).然后将二进制位从右向左依次填入对应的二进制格式x中,填完后,如果对应的二进制格式还有没填的x则设为0。
UTF-16UTF-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-32这个最简单,就是字符编号的整数二进制形式,4个字节。
但有个细节,就是字节的排列顺序,如果第一个字节是整数二进制中的最高位,最后一个字节是整数二进制中的最低位,那这种字节序就叫“天端”(Big Endian,BE),否则,就叫“小端”(Little Endian. LE)。对应的编码方式分别是UTF-32BE和UTF-32LE。
可以看出,每个字符都用4个字节表示,非常浪费空间,实际采用的也比较少。
Unicode编码小结Unicode给世界上所有字符都规定了一个统一的编号,编号范围达到110多万,但大部分字符都在65536以内。Unicode本身没有规定怎么把这个编号对应到二进制形式。
UTE-32/UTE-16/UTE-8都在做一件事,就是把Unicode编号对应到二进制形式,其对应方法不同而已。UTF-32使用4个字节,UTF-16大部分是两个字节,少部分是4个字节,它们都不兼容ASCII编码,都有字节顺序的问题。UTF-8使用1~4个字节表示,兼容ASCII编码,英文字符使用1个字节,中文字符大多用3个字节。
编码转换有了统一的Unicode编码后,就可以兼容不同类型的编码格式了,比如中文“西”字:
编码方式 | 十六进制 | 编码方式 | 十六进制 |
GBK | cef7 | UTF-8 | %u897F |
Unicode | \u897f | UTF-16 | 897f |
在不同的编码格式之间是如何通过Unicode编码进行兼容的呢?那就必须通过编码去转换,我们可以认为每种编码都有一个映射表,存储其特有的字符编码和Unicode编号之间的对应关系,这个映射表是一个简化的说法,实际上可能是一个映射或转换方法。
编码转换的具体过程可以是:一个字符从A编码转到B编码,先找到字符的A编码格式,通过A的映射表找到其Unicode编号,然后通过Unicode编号再查B的映射表,找到字符的B编码格式。通过这样转换,就可以实现不同编码格式的兼容了。
举例来说,“西”从GBK转到UTF-8,先查GB18030->Unicode编号表,得到其编号是\u897f,然后查Uncode编号->UTF-8表,得到其UTF-8编码: %u897F。
乱码的根源上面介绍了编码,现在我们来看一下乱码,产生乱码一般无非两个原因:一种就是比较简单的错误解析,另外一种就是比较复杂的,在错误解析的基础上还进行了编码转换。
错误解析一个英国人使用Windows-1252编码格式写了一个文件发送给一个中国人,然后中国人使用GBK解码打开,最后看到的这个文件就是乱码的;
这种情况下,之所以看起来是乱码,是因为看待或者说解析数据的方式错了。只要使用正确的编码方式进行解读就可以纠正了。很多文件编辑器,如EditPlus、NotePad++都有切换查看编码方式的功能,有的浏览器也有切换查看编码方式的功能,如火狐浏览器,在菜单“查看”→“文字编码”中即可找到该功能。
切换查看编码的方式并没有改变数据的二进制本身,而只是改变了解析数据的方式,从而改变了数据看起来的样子,这与前面提到的编码转换正好相反。很多时候,做这样一个编码查看方式的切换就可以解决乱码的问题,大多数仅是简单解析错误可以使用此方法解决。
错误的解析和编码转换如果怎么改变查看方式都不对,那很有可能就不仅仅是解析二进制的方式不对,而是文本在错误解析的基础上还进行了编码转换。我们举个例子来说明:
比如“西”字,本来的编码格式是GBK,编码(十六进制)是cef7。
这个二进制形式被错误当成了Windows-1252编码,解读成了字符“?÷”。
随后这个字符进行了编码转换,转换成了UTF-8编码,形式还是“?÷”,但二进制变成了 11111111111111111111111111111111。
这个时候再按照GBK解析,字符就变成了乱码形式“?÷”,而且这时无论怎么切换查看编码的方式,这个二进制看起来都是乱码。
这种情况是乱码产生的主要原因。
这种情况其实很常见,计算机程序为了便于统一处理,经常会将所有编码转换为一种方式,比如UTF-8,在转换的时候,需要知道原来的编码是什么,但可能会搞错,而一旦搞错并进行了转换,就会出现这种乱码。这种情况下,无论怎么切换查看编码方式都是不行的。
解决方法对付乱码的方法,如果是简单的方法,可以先使用编辑器试着重新解析,看看能不能解析回到正确的编码;但是如果遇到稍微复杂一点就不行,比如上述提到的错误的解析加上转换,那个用编辑器就不能找回正确的方法,这里推荐使用程序来解决;这里使用错误解析成“?÷”的乱码字符,我们来找回它正确的编码:
首先我们写个方法,先用个数组将所有的编码格式放进去(我这里演示就简单列举几个),然后使用循环去解析,再从输出的结果中,查找符合原编码的字符及对应编码。
程序是这样的:
最后的运行结果为:
最后我们得到的结果为“?÷”的乱码字符原编码是GBK,被错误解析为Windows-1252,这样我们就算是找到了字符的原编码了。
根据这个程序就能反着找到原来的编码,因为我们实际应用的编码格式有限,所以这种暴力反查找的方式还是很有用的,速度也很快。
当然,能找到的对应的原编码都是一些简单和不算非常复杂的乱码文件,如果文件被多次错误解析和多次格式转换,那其反破解原编码的难度无异于去破解无固定的保险柜密码。
总结通过本文可以清晰了解到计算机软件领域编码的分类,共分为非Unicode编码和Unicode编码,非Unicode编码主要是以ASCII体系为主,而Unicode编码最多应用的方案是UTF-8,这些不同的编码类型之间是可以通过一定的规则相互转换的。
编码和解码使用的不是同一套编码格式,会导致乱码现象的产生,因此在产生乱码时,我们一是可以借助一些编辑器或浏览器去更换编码来找到原先的格式,二是可以借助程序工具进行去暴力匹配,这种方法也比较直接有效。
——参考致谢《Java编程的逻辑》
电脑