分享

编程基础之编码与解码

 西北望msm66g9f 2020-04-23
关于编码与解码无论是编程还是日常的电脑使用都是避不开的,但是一般也没人会特意注意到这些,只有在某些特定场合下出现乱码的时候,才会根据网上教程修改一下编码格式。
在这里简单介绍一下相关的基础知识,让大家在知道怎么做的基础上,知道为什么要这样做。
此篇文章内容为黑马程序员关于编码与解码的学习笔记,B站上也有相关视频。大约三小时左右,大家看完文章如果觉得有兴趣的话,可以去B站看一下视频讲解,链接我会贴在文章末尾。
文章将会以下面思维导图进行展开叙述,可能有点长

什么是编码与解码

因为计算机底层只能存储01,  称为一个bit,8个bit组成一个字节。

所以存储类似于汉字、英文字符、符号字符等内容需要一个编码表,实现从字节到符号的转换。

从显示器上的字符到计算机中的字节称为编码

从计算机上的字节到显示器上的字符称为解码

二进制书写起来太麻烦,所以一般都用十六进制表示码值

常见编码表 ASCII

计算机发明的时候,基本只考虑了美国的需求,大概只需要128个字符,所以使用的是ASCII

其中编码的 32-126 表示可打印字符


0-31和127表示不可打印字符,下图列举一部分


水平制表符表示一个Tab

但是ASCII码表只对美国够用,于是各个国家的计算机厂商发明了各种各样的编码方式表示自己国家的字符。

西欧国家流行的是ISO 8859-1Windows-1252

在中国是GB2312GBKGB18030Big5

常见编码表 ISO-8859-1

ISO 8859-1又称Latin-1,它也是使用一个字节表示一个字符。

因为西欧的文字也都是字母拼接,只不过不是26个英文字母罢了,其中0到127与ASCII一样,128到255规定了不同的含义。

在128到255中,128到159表示一些控制字符,这些字符也不常用。

其中160到255表示一些西欧字符,如下图所示:


常见编码表 windows-1252

ISO 8859-1虽然号称是标准,用于西欧国家,但它连欧元()这个符号都没有,因为欧元比较晚,而标准比较早。

实际使用中更为广泛的是Windows-1252编码,这个编码与ISO 8859-1基本是一样的,区别只在于数字128到159。

Windows-1252使用其中的一些数字表示可打印字符,这些数字表示的含义,如下图所示:

这个编码中加入了欧元符号以及一些其他常用的字符。基本上可以认为,ISO 8859-1已被Windows-1252取代。

HTML5甚至明确规定,如果文件声明的是ISO 8859-1编码,它该被看做Windows-1252编码。

常见编码表 GB2312

美国和西欧字符用一个字节就够了,但中文显然是不够的。

中文第一个标准是GB2312,而且GB2312标准主要针对的是简体中文常见字符,包括约7000个汉字,不包括一些罕见词,不包括繁体字。

GB2312固定使用两个字节表示汉字,在这两个字节中,最高位都是1,如果是0,就认为是ASCII字符。

在这两个字节中: 
第一个字节范围是10100001(十进制161) 到 11110111(十进制247)。
第二个字节范围是10100001(十进制161) 到 11111110(十进制254)。

常见编码表 GBK

GBK建立在GB2312的基础上,向下兼容GB2312

也就是说,GB2312编码的字符的二进制表示,在GBK编码里是完全一样的。

GBK增加了一万四千多个汉字,共计约21000汉字,其中包括繁体字。

GBK同样使用固定的两个字节表示:
第一个字节范围是10000001(十进制129) 到 11111110(十进制254)。
第二个字节范围是0100000(十进制64) 到 01111110(十进制126) 和 10000000(十进制128) 到 11111110(十进制254)。

需要注意的是,第二个字节是从64开始的(64属于第一个bit为 0 的情况,和ASCII的编码重合了),也就是说,第二个字节最高位可能为0。

那怎么知道它是汉字的一部分,还是一个ASCII字符呢?

其实很简单,因为汉字是用固定两个字节表示的,在解析二进制流的时候,如果第一个字节的最高位为1,那么就将下一个字节读进来一起解析为一个汉字,而不用考虑它的最高位,解析完后,跳到第三个字节继续解析。

所以第二个字节从64开始是不会影响解码的,但第一个字节一定不能以 0 开头。

常见编码表 GB18030

GB18030向下兼容GBK,增加了五万五千多个字符,共七万六千多个字符。

包括了很多少数民族字符,以及中日韩统一字符。

用两个字节已经表示不了GB18030中的所有字符,GB18030使用变长编码,有的字符是两个字节,有的是四个字节。

在两字节编码中,字节表示范围与GBK一样。

在四字节编码中: 
第一个字节的值从10000001 (十进制129) 到 11111110(十进制254)。
第二个字节的值从00110000 (十进制48) 到 00111001(十进制57)。
第三个字节的值从10000001 (十进制129) 到 11111110(十进制254)。
第四个字节的值从00110000 (十进制48) 到 00111001(十进制57)。

解析二进制时,如何知道是两个字节还是四个字节表示一个字符呢?

很简单,看第二个字节的范围,如果是48到57就是四个字节表示,因为两个字节编码中第二字节都比这个大。

所以这样综合说明GB18030兼容GBK,兼容GB2312,兼容ASCIl

但是GB18030GBKGB2312这三个编码ISO8859-1是不兼容的哦。

常见编码表 Big5

Big5是针对繁体中文的,广泛用于台湾香港等地。

Big5包括1万3千多个繁体字,和GB2312类似,一个字符同样固定使用两个字节表示。

在这两个字节中: 
第一个字节范围是10000001(十进制129) 到 11111110(十进制254)。
第二个字节范围是01000000(十进制64)到 01111110(十进制126)和10100001(十进制161) 到 1111 1110 (十进制254)。

Big5GB18030GBKGB2312不兼容。

如果已经理解了上文,其实就能理解为什么Big5GB的三个编码为什么不兼容了。

编码表汇总

ASCII码是基础,一个字节表示,最高位设为0,其他7位表示128个字符。

其他编码都是兼容ASCII的,最高位使用1来进行区分。

西欧主要使用Windows-1252,使用一个字节,增加了额外128个字符。

中文大陆地区的三个主要编码GB2312GBKGB18030,有时间先后关系,表示的字符数越来越多,且后面的兼容前面的,GB2312GBK都是用两个字节表示,而GB18030则使用两个或四个字节表示。

香港台湾地区的主要编码是Big5

如果文本里的字符都是ASCII码字符,那么采用以上所说的任一编码方式都是一样的,不会乱码。

但如果有高位为1的字符,除了GB2312/GBK/GB18030外,其他编码都是不兼容的。

比如,Windows-1252和中文的各种编码是不兼容的,即使Big5GB18030都能表示繁体字,其表示方式也是不一样的,而这就会出现所谓的乱码。

常见编码表 Unicode

产生原因:

每个国家的各种计算机厂商都对自己常用的字符进行编码,在编码的时候基本忽略了别的国家的字符和编码,甚至忽略了同一国家的其他计算机厂商,这样造成的结果就是,出现了太多的编码,且互相不兼容。

Unicode 做了一件事,就是给世界上所有字符都分配了一个唯一的数字编号,这个编号范围从0x000000到0X10FFFF,包括110多万。

Unicode 中文翻译 统一码

但大部分常用字符都在0x0000到0XFFFF之间,即65536个数字之内。

每个字符都有一个Unicode编号,这个编号一般写成16进制,在前面加U+。

大部分中文的编号范围在U+4E00到U+9FA5。

1990年开始研发,1994年正式公布。
随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。
Unicode6.3版已发布(2013年11月)。在Unicode联盟网站上可以查看完整的6.3的核心规范。
Unicode定义了大到足以代表人类所有可读字符的字符集。
Unicode其实应该是一个码值表。Unicode的功用是为每一个字符提供一个唯一的数字码。而对数字码的存储规则的定义则需要依靠UTF-8/UTF-16/UTF-32
UTF-8/UTF-16/UTF-32是通过对Unicode码值进行对应规则转换后。编码保持到内存/文件中。UTF-8/UTF-16都是可变长度的编码方式。

Unicode就做了这么一件事,就是给所有字符分配了唯一数字编号。

它并没有规定这个编号怎么对应到二进制表示。

这是与上面介绍的其他编码不同的,其他编码都既规定了能表示哪些字符,又规定了每个字符对应的二进制是什么,而Unicode本身只规定了每个字符的数字编号是多少。

那编号怎么对应到二进制表示呢?

有多种方案,主要有UTF-32,UTF-16和UTF-8

UTF-32编码

字符编号的整数二进制形式,四个字节但这里四个字节的存储会有一个大小端的问题。

“大端”称为(Big Endian,BE),'小端”称为(Little Endian,LE) 

比如:


注意:

之所以有大端和小端两种方式,是因为硬件读写顺序的不同。

大端:数据的高字节保存在内存的低地址中,低字节保存到内存的高地址中,和我们的阅读习惯一致;小端则相反。

常用的X86结构是小端模式。arm支持大小端可配置 

采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。

UTF-16编码

在了解UTF-16编码方式之前,先了解一下另外一个概念——'平面'。

在上面的介绍中,提到了Unicode是一本很厚的字典,它将全世界所有的字符定义在一个集合里。

这么多的字符不是一次性定义的,而是分区定义。

每个区可以存放65536个(2^16)字符,称为一个平面(plane)。

目前,一共有17个(小于2^5)平面(65536*17=1114112 也就是110多万),也就是说,整个Unicode字符集的大小现在是2^21

最前面的65536个字符位,称为基本平面(简称BMP),它的码点范围是从0到2^16-1,写成16进制就是从U+0000到U+FFFF。

所有最常见的字符都放在这个平面,这是Unicode最先定义和公布的一个平面。

剩下的字符都放在辅助平面(简称SMP),码点范围从U+010000到U+10FFFF。

基本了解了平面的概念后,再说回到UTF-16。

UTF-16编码介于UTF-32与UTF-8之间,同时结合了定长和变长两种编码方法的特点。
它的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。
也就是说,UTF-16的编码长度要么是2个字节(U+0000到U+FFFF),要么是4个字节(U+010000到U+10FFFF)。
那么问题来了,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?
为了将两个字节的UTF-16编码与四个字节的UTF-16编码区分开来。
Unicode编码的设计者将基本平面的 0xD800 - 0xDFFF 保留下来,称为代理区(Surrogate):
辅助平面的字符位共有2^20个,因此表示这些字符至少需要20个二进制位。
UTF-16将这20个二进制位分成两半,前10位映射在U+D800到U+DBFF,称为高代理位(H),后10位映射在U+DCO0到U+DFFE,称为低代理位(L)。
这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

如果 U>0x10000,我们先计算U=U-0x10000,然后将U写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U的UTF-16编码(二进制)就是:110110yyyyyyyyyy110111xxxxxxxxxx

按照上述规则,Unicode编码0X10000-0×10FFFF的UTF-16编码有四个字节,前两个字节的高6位是110110,后两个字节的高6位是110111

可见,前两个字节的取值范围(二进制)是1101100000000000到1101101111111111,即QXD800-0xDBFF。

后两个字节取值范围(一进制)是1101110000000000到1101111111111111,即OXDCOO-0XDFFE。

因此,当我们遇到两个字节,发现它的码点在U+D800到U+DBFE之间,就可以断定,紧跟在后面的两个字节的码点,应该在U+DC00到U+DFFE之间,这四个字节必须放在一起解读。

举例说明:

UTF-32一样,UTF-16也有UTF-16LE(小端)和UTF-16BE(大端)之分

注意:

UTF-16常用于系统内部编码,我们平常说的“Unicode编码是2个字节”这句话,其实是因为windows系统默认的Unicode编码就是UTF-16。

在常用基本字符上2个字节的编码方式已经够用导致的误解,其实是可变长度的。

在没有特殊说明的情况下,常说的Unicode编码可以理解为UTF-16编码,而且是UTF-16BE(大端)编码

UTF-16UTF-32节省了很多空间,但是任何一个字符都至少需要两个字节表示,对于美国和西欧国家而言,还是很浪费的。所以就出现了UTF-8

UTF-8编码

UTF-8就是使用变长字节表示,每个字符使用的字节个数与其Unicode编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多,使用的字节个数从1到4个不等。

具体来说,各个Unicode编号范围对应的二进制格式模板

如下图所示:图中的x表示可以用的二进制位,而每个字节开头的1或0是固定的。

小于128的(即OX00-0X7F之间的字符),编码与ASCII码一样,最高位为0。

其他编号的第一个字节有特殊含义,最高位有几个连续的1表示一共用几个字节表示,而其他字节都以10开头。

4字节模板有21个x,即可以容纳21位二进制数字。

Unicode的最大码位0×10FFFF也只有21位。

举例说明:

注意:UTF-8UTF-32/UTF-16不同的地方是UTF-8是兼ASCII的,对大部分中文而言,一个中文字符需要用三个字节表示。

UTF-8的优势是网络上数据传输英文字符只需要1个字节,可以节省带宽资源。

所以当前大部分的网络应用都使用UTF-8编码,因为网络应用的代码编写全部都是使用的英文编写,占据空间小,网络传输速度快。

BOM编码

比如一个文本软件,在打开一个文件的时候,如何判断这个文件是使用的什么编码呢,该用什么编码进行解码呢?那么就需要通过BOM(Byte Order Mark)来指明了。

Unicode标准建议用BQM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符“零宽无中断空格”。

这个字符的编码是FEFF,而反过来的FFFE(UTF-16)FFFE0000(UTF-32)Unicode中都是未定义的码位,不应该出现在实际传输中。

BOM主要用来指明编码方式与大小端模式,比如新建一个文本文件,指定编码格式

ANSI表示使用系统默认编码我们如果选定一种格式进行保存

可以发现,即使没有内容,还是会有三个字节的大小,这就是因为有BOM来指定了文本文件的编码方式。

想查看这些字节具体是什么,可以编写程序(例如Java)用字节流读取文本文件进行显示

注意:UTF-8不需要BOM来表明字节顺序,因为单个字节不涉及大小端的问题,但可以用BOM来表明文件是UTF-8的编码方式。

根据BOM的规则,在一段字节流开始时,如果接收到以下字节,则分别表明了该文本文件的编码。

而如果不是以BOM开头,那程序则会以ANSI,也就是系统默认编码读取。

乱码原因

乱码产生的根源一般情况下可以归结为三方面即:

  • 编码引起的乱码
  • 解码引起的乱码
  • 缺少某种字体库引起的乱码(这种情况需要用户安装对应的字体库)

其中大部分乱码问题是由不合适的解码方式造成的

乱码可逆情况

其中缺少字体,只需要安装对应的字体库即可解决乱码,比如Windows系统在C:\Windows\Fonts目录下会有安装好的字体库列表。

安装字体库比较简单,下载后解压,然后复制到对应系统的Fonts目录下。

解码方式和编码方式不一致的情况,只需要让解码方式和编码方式一致即可让乱码恢复。

乱码不可逆情况

总结补充

Unicode 有三种编码方式,只有UTF-8兼容ASCII编码,其他不兼容

资料链接:

https://www.bilibili.com/video/BV1HJ411E7d3?p=26

欢迎阅读到这里,如果觉得文章有用,可以点击下方的广告链接,也算是对我的支持(当然,不强求哦)

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多