Skip to content

Latest commit

 

History

History
64 lines (36 loc) · 9.18 KB

Python_binary1.md

File metadata and controls

64 lines (36 loc) · 9.18 KB

Python字符、编码与二进制(一)

我们都知道计算机以二进制方式运作,所有的信息最终都被存储为二进制字节。每个字节包含8个比特。对于数字来说,它们以二进制的方式存储是比较方便的,例如“5”可以存储为3比特:0b101, 0b1111则表示整数15。那么,计算机如何来存储字符呢?

字符集

既然计算机可以很方便得存储数字,人们便想到了将字符映射为数字再转为二进制存储既可。字符与数字的映射就被称作字符集。最常见的字符集就是ASCII字符集。ASCII字符集将128个字符(分为95个可打印字符和33个控制字符)映射到了0~127这128个整数上面。这其中就包括了英文26个字母的大小写和各种键盘上能够看见的符号(+-*/,.),以及许多诸如制表符\t、换行符\n等特殊字符。一旦映射成为了整数,存储就很方便了,因为整数可以直接以二进制形式存储。举个例子,字符'p'在ASCII码表中对应的整数是112,它表示为二进制是0b1110000,共7个比特。

在Python中,我们可以利用内建函数(ordchr)很方便得获取一个字符对应的ASCII码值,以及通过一个整数来获取对应的ASCII码字符:

print(ord('p'))
# 112
print(chr(112))
# 'p'

有了ASCII字符集,我们就可以存储和传输部分字符了。在这里面需要强调一点的是,将字符映射为整数和将整数存储为二进制是两个过程,我们将前者称为字符集映射,而后者称为编码(encoding)。只不过ASCII直接按照整数所对应的二进制进行存储,看起来仿佛映射与编码是同一个过程。后面我们会看到不同的存储方式。

ASCII码有两个不足之处:

  1. 计算机通常以字节为基本存储单位,即8个比特,而ASCII码只使用了7个比特,有了1个比特的浪费;
  2. ASCII只能表示128个字符,世界上可显示的字符有成千上万个,仅仅中文简体字就有两千多个,这些字符如何存储?

针对上述两点不足,人们做出了一些改进。例如,利用上ASCII码的第8个比特,这样又可以多收入128个字符。但是这还是远远不够,且另一个新问题是,人们都在提出各自的扩展方式,没有一个统一的标准。例如,甲认为整数150表示’©‘,而乙按照自己的标准认为150表示'¥'。这样,甲发送给乙的文档中所有应该是'©'的地方全部被乙翻译为了'¥',造成了歧义。

对于第一个字符集严重不足的问题,人们提出了多字节字符集来满足需求。以汉字为例。我国最早的汉字字符集是GB2312。里面包含了99.7%以上的常用中文字符,并且包括了拉丁字母、希腊字母、平假名片假名等等其他字符。GB2312以区位码(也是一个整数)来表示每个字符,并以两个字节来存储。举个例子,字符'我'的区位码是4650,其二进制存储内容是0b1100111011010010, 表示为十六进制为0xCED2。这里我们发现,数字4650的二进制表示为0b1001000101010,与上面GB2312存储的字节并不一致。这也印证了前面我们所说的映射与存储是两个过程。

GB2312在设计时兼容了ASCII码,将所有中文字符都放在了128的后面。所以,利用GB2312映射出来的拉丁字母和利用ASCII码映射来的拉丁字母是完全一致的。

GB2312虽然涵盖了足够多的字符,但仍旧有一些字符没有收录进来。因而后续有了许多扩展,比较流行的是GBK和目前最新的GB18030。其中GB18030是我国大陆强制使用的最新字符集,而我国台湾地区则使用的是BIG5字符集,其主要收录了繁体字符。

事实上,世界各地都在提出适用于自身的字符集,而这些字符集之间几乎无法兼容(大家都兼容了ASCII)。这就导致了一个巨大的兼容性问题。一个利用GB2312映射后的文章,在一台采用了法语的某个字符集的机器上打开一定是乱码。因此,一些标准化组织便开始致力于提出一个全球统一的字符集,以便于世界各地的各种字符都可以映射为一个标准的统一的数字。这其中最有名的标准是ISO1064和Unicode。事实上,由于两者在很久以前就宣布互相兼容,共同扩展,因此,现今我们提到的统一字符集通常都指的是Unicode。

Unicode统一了世界各地不同字符的映射方式,拉丁字母、汉字、日文、韩文、法文、德文等等都可以在Unicode映射表中找到唯一的整数与之对应。这大大提高了字符在不同语种国家之间的传递。只要大家都遵循Unicode标准,那么所有字符都可以被正确得显示出来。

那么Unicode怎么做的?

Unicode将所有字符分为了17个平面,每个平面中的每个字符均用4位16进制数来表示。也就是说,Unicode使用的整数的范围,利用16进制来表示是00000~FFFFF。其中第一位的0~F表示第0~16平面,后四位0000~FFFF表示该平面内对应的字符。如果转化为整数,Unicode的字符范围是0~1114112。Unicode的第0个平面被称作Basic Multilingual Plane (BMP)。我们目前使用的绝大多数字符,包括各个国家的字符,都涵盖在了这个平面内。值得一提的是,Unicode也完全兼容了ASCII字符集。所以在Unicode第0平面的0~127个整数和ASCII字符集是完全一致的。

Unicode字符的表示方式是'U+'开头,后面加上字符对应的16进制数值。例如,字符'a'的Unicode表示方式是'U+0061',写成整数是97,和ASCII一致,而字符'我'的Unicode表示方式为'U+6211',写成数字是25105。

Unicode字符集也在不断地容纳新的字符进来,例如,Emoji表情符号也在不断加入Unicode家庭中。目前,Unicode已经进入了11.0.0版本,以收录超过13万个字符。

Unicode编码方式

前面一直在强调,映射和编码是两个过程。上面我们主要介绍的是Unicode字符集的映射方式,即如何将一个字符映射为一个整数。下面我们重点介绍一下计算机如何来存储这个整数。

最简单的想法,像ASCII码一样,字符映射成什么整数,就存什么整数。例如,'我'的存储方式就是0x6211。像这样直接存储Unicode字符集的编码方式被称作UTF-16,其中UTF表示'Unicode Transformation Format',而16则表示该编码方式采用16比特(即2字节)来存储Unicode字符。UTF-16这种编码方式存在几个问题。一是只采用两个字节,如何能够表示共17个平面的Unicode字符集呢?原因是在各个平面中,存在许多未使用的码字,对于超平面(1以上平面)的字符,UTF-16利用了该平面与BMP平面中未使用的码字构成代理对(4个字节)来表示;二是对于ASCII码而言,两个字节过于浪费(本身只需要7个比特),而ASCII码又是我们最常用的字符集,因而若采用UTF-16编码会产生巨大的浪费;三是由于网络中存在大端(big-endian)和小端(little-endian)两种字节序,UTF-16编码在传输过程中要指明本段编码是大端还是小端,否则解码时会产生歧义。例如字符'我'利用UTF-16编码后以大端传输为0x6211,而收方按小端序接收则变成了0x1162,解码后成了字符'ᅢ',造成了歧义。UTF-16解决方法称作Byte Order Mark (BOM)。在Unicode中存在一个特殊字符'U+FEFF',而FFFE则不存在于Unicode中。因而UTF-16要求所有编码文档的最开始两个字节存储BOM,若存储的是0xFEFF,则该文档为大端字节序,若存储的是0xFFFE,则为小端字节序。

另外一种不怎么使用的编码方式是UTF-32,顾名思义,其采用32比特(4个字节)存储Unicode字符(实际上存储的是UCS-4字符集)。因为这种方式过于浪费空间,基本上不会用到。

UTF-8

鉴于UTF-16和UTF-32的各种不足,UTF-8编码横空出世,并成为了全世界最流行的Unicode编码方式。UTF-8并不意味着以单字节存储字符,而是以变长字节来存储。字节长度为1~4字节不等。UTF-8的编码方式比较简单:

  1. 对于ASCII码,UTF-8采用单字节存储,且最高位为0,这样,ASCII码和UTF-8编码可以直接兼容;
  2. 对于其他码字,UTF-8采用2~4字节存储,其中第一个字节的头部以'110'、'1110'、'11110'表明该字符是2个、3个、4个字节存储的(前置1的个数),而剩余的几个字节的头部全部是'10',每个字节剩余位置存储Unicode字符的码字。

举个例子:字符'a'的UTF-8编码同ASCII码一样,就是0x61;而字符'我'则需要三个字节编码,首字节为0b11100110 ,第二字节为0b10001000, 第三字节为0b10010001。我们把字节的前置位拿去,剩下的比特组合起来是0b0110001000010001,其16进制表示是0x6211正是我们前面看到的字符'我'的Unicode字符映射的整数。

UTF-8解决了字节序问题。

因为UTF-8把每个字符字节的头都标了出来,所以当机器读到了0xE6(字符'我'的UTF-8编码的第一个字节)时,就知道继续读两个字节并进行解码,而不是按照小端大端做反序再做解码。