深入刨析字符乱码

362 阅读14分钟

一.背景

在日常Ulink活动跟进过程中,有时活动页面打开的时候会遇到乱码的情况(如下图所示),于是就想乱码到底是怎么产生的,遇到乱码的情况应该怎么去解决,带着这些问题,我去查阅了相关的资料,在这里整理成文章分享给大家,希望对大家有所帮助。

img

二.乱码产生的原因

我们都知道,计算机是只认识0和1的二进制数的,所以不管是字母,汉字,或者符号,都是以某种编码方式转换成二进制数据存放在计算机中,需要显示的时候,就用相同的编码方式把二进制数据解码出来就可以了。那么这就很好理解乱码的产生了,如果我们用A编码方式将字符进行编码,然后用B编码方式来解码,解码出来的就肯定是乱码。我们用一个比较风趣的故事可以很形象的说明这个问题:有一天你带着女朋友去见一个老外朋友,见面时老外很热情的赞赏道 your girl friend is so beautiful,然后你本着中华名族谦逊有礼的优良传统,回答“where”,这时你的老外朋友应该就是这个表情了…

img

这个故事中,你想表达的“哪里”这个内容用博大精深中文编码后是谦虚的意思,但是老外用他自己的思维去解码后就是哪里的本意,当然会不明白什么意思了。

三.相关概念介绍

要搞清楚乱码的问题,我觉得应该从这些很容易混淆的基本概念说起。譬如什么是字符,字符编号,字符集等。

1.字符

字符是具有语义值的最小文本单位,包括字母、数字、运算符号、标点符号和其他符号,以及一些功能性符号,字符也是数据结构中最小的数据存取单位,有的人可能会有一些误解,认为英文字母或者特殊符号占一个字节,而中文汉字是占2个字节,其实这种想法不准确的,字符在计算机中所占的字节数和它的字符编码有关,在GB2312字符编码中,一个汉字是占2个字节,但是在UTF-8字符编码中,是占3个字节,所以在不知道哪种字符编码的时候说字符占多少个字节的说法是不够严谨的。

2.字符编号

字符编号是用来表示一个字符的号码,类似我们的身份证号码,具有不可重复性,同一个字符在不同的字符集中,他的字符编号也是不一样的,这个也很好理解,就像你在中国的护照号码和你移民到了美国的护照号,你还是你,但是护照号已经不一样了。

3.字符集

字符集是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集有:ASCII字符集、GB2312字符集、Unicode字符集等。不同的字符集包含的字符个数也不相同,比如ASCII字符集收录了128个字符,而GB2312收录了7445个字符。平常大家所说的字符集准确的讲应该叫编号字符集,它是指用字符编号来标示某个字符集中每一个字符后生成的字符集合,听着有点绕,为啥要给字符加个编号呢?主要还是能方便定位找到某个指定的字符,这个和每个人生出后都会起一个姓名一样。

4.字符编码

字符编码也称字集码,是把字符集中的字符,编码后生成指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。这是百度百科对它的专业定义,本人的理解是字符集中的字符标号和实际存储的二进制数据之间的映射关系,譬如在ASCII字符编码中字母A的编号是65,所以它存入计算机时的值就是’01000001’。

img

有些人比较容易混淆字符集和字符编码这两个概念,通俗的讲,字符集是用字符编号标示后的字符集合,而字符编码是指字符用什么方式转化成二进制数据的规则。

5.编码

编码是信息从一种形式或格式转换为另一种形式的过程,也称为计算机编程语言的代码(简称编码)。用预先规定的方法将文字、数字或其它对象编成数码,或将信息、数据转换成规定的电脉冲信号。通俗的理解就是将字符按一定的规则转换成二进制数据。

6.解码

与编码相对,解码是一种用特定方法,把数码还原成它所代表的内容或将电脉冲信号、光信号、无线电波等转换成它所代表的信息、数据等的过程。解码是将接受到的符号或代码还原为信息的过程,与编码过程相对应。

四.常用字符集

1.ASCII字符集

文章开头也说到数据都是以二进制的形式存储在计算机中,那哪些二进制数据表示哪些字符,每个人都有一套自己的规则,这样就不能互相通信,为了解决这个问题,美国相关标准化组织就出台了ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)编码,统一了二进制数据对应字符的规则,ASCII字符集一共包含128个字符(0-127)。其中有33个控制字符(不可显示字符),52个英文字母(包括大小写),0~9十个阿拉伯数字,其余为一些特殊符号。它使用一个字节的后7位来表示某个字符,对应的二进制编码是0000 0000 ~ 0111 1111,其中最高位一般为0,但有时也被用作一些通讯系统的奇偶校验位,就像上面举例的大写字母A的二进制编码就是’01000001’。

img

2.ISO8859-1字符集

随着计算机技术的慢慢发展,人们发现ASCII字符集的128个字符已经不能满足他们的需求了,于是ISO8859-1字符集就诞生了,它共有256个字符,向下兼容ASCII,编码范围是0x00-0xFF,其中0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。它属于西欧语系中的一个字符集,支持希腊语、丹麦语、阿拉伯语等,和ASCII字符集一样,ISO8859-1字符集也是使用默认的字符编码来把字符编号转换成二进制数据存储在计算机中。

img

3.GB2312字符集

GB2312字符集全称为《信息交换用汉字编码字符集·基本集》,由原中国国家标准总局发布,1981年5月1日实施。计算机到中国的时候,已经没有空余的字节空间存放中文了,而且中文有那么多汉字,怎么也放不下,所以只好另外再新建一种字符集,于是GB2312字符集就产生了,它使用两个字节来表示一个汉字,前面的一个字节(高字节)使用0xA1-0xF7这个区间,后面一个字节(低字节)使用0xA1-0xFE这个区间,这样我们就可以组合出大约存放7000多个字符的空间了。其中包括6763个汉字(一级汉字3755个,二级汉字3008个),已经可以覆盖99.75%的汉字使用频率,基本满足了汉字的计算机处理需要。GB2312字符集还把符号、罗马希腊字母、日文假名都编进去了,就连ASCII 里本来就有的数字、标点、字母都重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符。全角和半角通过高字节的最高位是1还是0来判断,1表示全角,按照两个字节来解码,0表示半角,按照1个字节来解码。那这么多字符是怎么排列的呢?GB2312中对所收汉字进行了“分区”处理,每个区含有94个汉字/符号。这种表示方式也称为区位码。各区包含的字符如下:01-09区为特殊符号;16-55区为一级汉字,按拼音排序;56-87区为二级汉字,按部首/笔画排序;10-15区及88-94区则未有编码。下面举一个具体例子说明是怎么存放的。GB2312编码是将区码和位码分别加上160(十进制)的偏移量之后(因为要兼容ASCII码),再转化成二进制存放到计算机中的,譬如“有”字的区位码是5148,区码51和位码48分别加上160以后就是211和208,所以它在计算机中的存储值就是’1101001111010000’。

img

4.Unicode字符集

计算机到了各个国家后,每个国家都各自搞了一套能兼容ASCII字符集但又不能互相兼容的编码方案,这样就会导致一个国家的二进制编码到了另外一个国家不能被正常解码的问题,对数据传输带来很大的障碍(就像古代秦国统一其他六国后也面临着这个问题),这时候就迫切需要有一种能够容纳世界上所有语言和符号的字符集,于是Unicode字符集就这样万众期待的诞生了。在Unicode字符集中,每一个字符都有一个独一无二的编码,这也正好印证了它的名字,Unique(唯一)和code(编码)两者的结合就成了Unicode。它一共有17个平面(0到16),每个平面里又分了很多页,每一页里又有很多行,而我们的字符就按照各自的字符编号,静静的存放在每一行里。目前17个平面中目前只用到0号、1号、2号、3号和14号平面,其中我们的汉字在0号、2号和3号平面,其它文字在0号、1号和14号平面;譬如“当”字就存放在第0号平面,第6页,第96行,他的编号就是5F53。

img

Unicode的产生标志着字符编码领域进入了一个全新时代,不仅是因为它存储了世界上所有的字符,而且它还把字符集和字符编码的定义严格的区分开来,在Unicode诞生之前,所有的字符集都是和字符编码捆绑在一起的,这种方式的缺点在与字符和字节流之间的耦合度太高,导致它的可扩展性不强。而Unicode考虑到了这点,可以有不同的字符编码(UTF-8、UTF-16等),也就是说,决定最终字节流的是字符编码,譬如对字母“A”进行UTF-8编码,得到的字节流是0x41,用UTF-16进行编码,得到的却是0x00 0x41。下图是GB2312和Unicode的对比,很好的说明了这个问题。

img

在Unicode众多编码方式中,我们最常见到的就是UTF-8,下面就详细说一说这个编码方式。就像上面所说,字符编码其实是从字符编号到实际存储二进制字节流的映射,下面这张表可以分析它是怎么实现这个映射关系的。表中x代表序号部分,把各个字节中所有x拼接在一起就组成了在Unicode字符集中的字符编号,其中第一个字节可以是0,110,1110开头,这样以此类推,但是从第二个字节开始,后面每个字节都只能以10开头,不同的数字开头分别表示的意思如下:

img

  • 如果一个字节的第一位是0开头的,就表示这个字符是单字节字符,只占用一个字节空间,剩下的7个bit就表示在Unicode中的字符编号了。
  • 如果一个字节是110开头的,就表示这个字符是双字节字符,占用了2个字节空间,第一个字节剩下的5个bit加上后一个字节除了10以外的6个bit一起表示在Unicode中的字符编号。
  • 如果一个字节以1110开头,就表示这个字符是三字节字符,占用3个字节空间,第一个字节剩下的4个bit加上后两个字节除了10以外的12个bit一起表示在Unicode中的字符编号。四字节字符同理,就不重复说明了,为了加深理解,下图举了几个例字说明了一个具体字符到它的Unicode编号,编码后的字节序列,到用UTF-8编码后的二进制和十六进制的对应关系。
  • img

五.乱码产生的场景

因为篇幅关系,这里举一个最常见的HTML页面的乱码场景,我们项目指定使用UTF-8编码,但是在html文件中,我们使用GBK编码,用浏览器运行后就出现了乱码。

img

下面我们来具体分析一下是怎么产生这些乱码的,文章一开始也说了,乱码的产生其实就是编码和解码使用的方式不一致产生的,UTF-8和GBK都很好的兼容了ASCII字符集,所以它们对单字节的英文字母或者数字解码时不会有什么差别,但是对中文汉字就不一样了,对UTF-8而言,一个汉字占3个字节,所以对本来要显示的这段文本编码后产生的二进制字节流如下图左边部分所示,而对GBK而言,一个汉字是占两个字节的,第一个字节的十进制值大于128,则认为这个是表示中文汉字,会将它和后面一个字节连起来一起解码,所以这段文本用GBK解析后重新分组排列就变成了下图右边部分所示,除了第五组和第十组是一个字节,其他每组都是两个字节(因为它们的第一个字节的十进制值都大于128),其中第五组是因为第一个字节小于128,第十组是因为后面没有字节了。我们通过每组的二进制值在GBK字符集中找到它对应的位置,查到的具体字符和浏览器显示的是一致的。

img

如果想让网页显示正常的文本其实很简单,只要将html中的编码改成UTF-8就可以了。

六.总结

从第一个字符集的诞生到后面不断有新的字符集产生,其实都是因为随着计算机的发展,原有的字符集满足不了当前的需求的原因,然后才会有不同的编码方式来编码和解码,最终产生的乱码,从上面的例子中也不难看出,其实类似我们平常看一句句子,用不同的断句方式,可能会产生完全不一样的句意,希望通过这篇文章,可以帮大家把字符集,字符编码理清楚,遇到乱码的时候,只要能分析好每次编码和解码使用的方式是否一致,这样乱码的问题自然会迎刃而解了。

更多精彩内容,尽请关注腾讯VTeam技术团队微信公众号和视频号

原作者:裘维清

未经同意,禁止转载!