在你的网络冲浪生涯里,我想你或多或少有这样的疑问:为什么传说中只能读懂0和1的计算机能显示如此五花八门的内容?为什么明明办的100兆的宽带,撑死就只有10几兆的下载速度?为什么有时打开文件会出现这样像中毒一般的“火星文”?
那么上完今天的课,我想这些疑问都能迎刃而解。今天的课主要有两大块内容,分别是编码和文件读写。
我们先来看编码。编码的本质就是让只认识0和1的计算机,能够理解我们人类使用的语言符号,并且将数据转换为二进制进行存储和传输。
这种从人类语言到计算机语言转换的形式,就叫做编码表,它让人类语言和计算机语言能够一一对应起来。
要了解编码,我们还得先来聊聊二进制。由于有二进制,0和1这两个数字才能像“太极生两仪,两仪生四象,四象生八卦”一样,涵盖容纳世间所有的信息。
说起二进制,我就想起了西游记里的二进陈家庄……噢不对,是更久远的周幽王烽火戏诸侯。所以接下里我会用烽火这种古老的信息传递形式,来比喻说明计算机是怎么传输和存储数据的。
假设我们都是看守城墙的小兵,你在烽火台A上,我在烽火台B上,只要你那边来了敌人,你就点着烽火台通知我。
如果只有一个烽火台,那么只有“点着火”和“没点火”两种状态,这就像电子元件里“通电”和“没通电”的状态,所以只有0和1.
但是你光告诉我来敌人还不够啊,还得告诉我敌人的数量有多少,让我好call齐兄弟做好准备。现在问题是你要怎么通知我敌人的数量呢?
所以,我们之间就约定了特别的“暗号”,来通知彼此敌情。
现在有两座烽火台,右边为第1座,左边为第2座。我们约定,当没有烽火台被点着的时候,表示没有敌人(00);只点着第一座烽火台的时候,表示来了一个敌人(01);只点着第二座烽火台的时候,表示来了2个敌人。(10,逢二进一)
当两座烽火台都被点着的时候(11),就表示来了3个人。
也就是这样的对应关系:
二进制 - 十进制
00 - 0
01 - 1
10 - 2
11 - 3
所以两个二进制位可以表示十进制的0,1,2,3四种状态。
现在你应该可以听得懂这个笑话:世界上有10种人,懂二进制和不懂二进制的。
我们继续往下推,当有三座烽火台的时候,我们可以表示0~7八种状态(也就是2的3次方)。
以此类推,当有八座烽火台的时候,我们就能表示2的8次方,也就是256种状态,它由8个0或1组成。
00000000 表示状态0: 烽火全暗,一个敌人没有,平安无事,放心睡觉。
11111111 表示状态255:烽火全亮,来了255个敌人。起来打啊!
用来存放一位0或1,就是计算机里最小的存储单位,叫做【位】,也叫【比特】(bit)。我们规定8个比特构成一个【字节】(byte),这是计算机里最常用的单位。
bit和byte长得有点像,可别混淆!1 byte = 8 bit,也就是1字节等于8比特。
这些计算机单位,可与我们息息相关,你的手机“流量”,就是这么计算的:
而百兆宽带,下载速度最多能达到十多兆,是因为运营商的带宽是以比特每秒为单位的,比如100M就是100Mbit/s。
而我们常看到的下载速度KB却是以字节每秒为单位显示的,1byte = 8bit,所以运营商说的带宽得先除以8,你的百兆宽带下载速度,也就是十几兆了。
二进制居然能牵扯出这么多生活中的问题,你是否也很意外?哈哈,其实生活处处是知识呀。
好,咱们言归正传,来看让人类语言和计算机语言能够一一对应起来的【编码表】。
计算机一开始发明的时候,只是用来解决数字计算的问题。后来人们发现,计算机还可以做更多的事,正所谓能力越大,责任越大。但由于计算机只识“数”,因此人们必须告诉计算机哪个数字来代表哪个特定字符。
于是除了0、1这些阿拉伯数字,像a、b、c这样的52个字母(包括大小写),还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,理论上每个人都可以有自己的一套规则(这就叫编码)。
但大家如果想要互相沟通而不造成混乱,就必须使用相同的编码规则。如果使用了不同的编码规则,那就会彼此读不懂,这就是“乱码”的由来。
为了避免乱码,一段世界历史就此启动。一开始,是美国首先出台了ASCII编码(读音:/ˈæski/),统一规定了常用符号用哪些二进制数来表示。
因为英文字母、数字再加上其他常用符号,也就100来个,因此使用7个比特位(最多表示128位)就够用了,所以一个字节中被剩下的那个比特位就被默认为0。
再后来呢,这套编码表传入欧洲,才发现这128位不够用啊。比如说法语字母上面还有注音符,这个怎么区分?得!把最后一个比特位也编进来吧。因此欧洲普遍使用一个全字节(8个比特位)进行编码,最多可表示256位,至此,一个字节就用满了!
但是前面的状态0-127位可以共用,但从状态128到255这一段的解释就完全乱套了,比如135在法语,希伯来语,俄语编码中完全是不同的符号。
当计算机漂洋过海来到中国后,问题又来了,计算机完全不认识博大精深的中文,当然也没法显示中文;而且一个字节的256位都被占满了,但中国有10万多个汉字,256位连塞牙缝都不够啊。
于是中国科学家自力更生,重写了一张编码表,也就是GB2312,它用2个字节,也就是16个比特位,来表示绝大部分(65535个)常用汉字。后来,为了能显示更多的中文,又出台了GBK标准。
不仅中国,其他国家也都搞出自己的一套编码标准,这样的话地球村村民咋沟通?日本人发封email给中国人,两边编码表不同,显示的都是乱码。
为了沟通的便利,Unicode(万国码)应运而生,这套编码表将世界上所有的符号都纳入其中。每个符号都有一个独一无二的编码,现在Unicode可以容纳100多万个符号,所有语言都可以互通,一个网页上也可以显示多国语言。
看起来皆大欢喜。但是!问题又来了,自从英文世界吃上了Unicode这口大锅饭,为迁就一些占用字节比较多的语言,英文也要跟着占两个字节。比如要存储A,原本00010001就可以了,现在偏得用两个字节:00000000 00010001才行,这样对计算机空间存储是种极大的浪费!
基于这个痛点,科学家们又提出了天才的想法:UTF-8(8-bit Unicode Transformation Format)。它是一种针对Unicode的可变长度字符编码,它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,而当字符在ASCII码的范围时,就用一个字节表示,所以UTF-8还可以兼容ASCII编码。
Unicode与UTF-8这种暧昧的关系一言以蔽之:Unicode是内存编码的规范,而UTF-8是如何保存和传输Unicode的手段。
将上述这段波澜壮阔、分久必合的编码史浓缩成一个表格表示,就是:
人类语言千变万化,我们有《新华字典》《牛津英语字典》这样的辞书来记录和收纳。可以说,这些编码表就是计算机世界的字典辞书,它们同样也是人类智慧的结晶。
因为二进制是由一堆0和1构成的,过长的数字对于人的阅读有很大障碍,为了解决这一问题,也减少书写的复杂性,我们又引入了八进制和十六进制。
为什么偏偏是16或8进制?2、8、16,分别是2的1次方、3次方、4次方。这一点使得三种进制之间可以非常直接地互相转换。
8进制是用0,1,2,3,4,5,6,7;16进制是用0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f来表示。比如说,字母K在ASCII编码表用不同进制表示的话是这样的:(你并不需要知道具体的转换规则)
第0,计算机是有自己的工作区的,这个工作区被称为“内存”。数据在内存当中处理时,使用的格式是Unicode,统一标准。
在Python3当中,程序处理我们输入的字符串,是默认使用Unicode编码的,所以你什么语言都可以输入。
第1,数据在硬盘上存储,或者是在网络上传输时,用的是UTF-8,因为节省空间。但你不必操心如何转换UTF-8和Unicode,当我们点击保存的时候,程序已经“默默地”帮我们做好了编码工作。
第2,一些中文的文件和中文网站,还在使用GBK,和GB2312。
基于此,有时候面对不同编码的数据,我们要进行一些操作来实现转换。这里就涉及接下来要讲的【encode】(编码)和【decode】(解码)的用法。
编码,即将人类语言转换为计算机语言,就是【编码】encode();反之,就是【解码】decode()。它们的用法如下图所表示:
你可以尝试一下,抄写下面的代码来运行。1~2行是encode()的用法,3-4行是decode()的用法
print('大师兄'.encode('utf-8'))
print('大师兄'.encode('gbk'))
print(b'\xe5\xa4\xa7\xe5\xb8\x88\xe5\x85\x84'.decode('utf-8'))
print(b'\xb4\xf3\xca\xa6\xd0\xd6'.decode('gbk'))
运行结果:
b'\xe5\xa4\xa7\xe5\xb8\x88\xe5\x85\x84'
b'\xb4\xf3\xca\xa6\xd0\xd6'
大师兄
大师兄
将人类语言编码后得到的结果,有一个相同之处,就是最前面都有一个字母b,比如b’\xb4\xf3\xca\xa6\xd0\xd6’,这代表它是bytes(字节)类型的数据。我们可以用type()函数验证一下,请你直接运行以下代码。
print(type('大师兄'))
print(type(b'\xb4\xf3\xca\xa6\xd0\xd6'))
运行结果:
<class 'str'>
<class 'bytes'>
所谓的编码,其实本质就是把str(字符串)类型的数据,利用不同的编码表,转换成bytes(字节)类型的数据。
我们再来区分下字符和字节两个概念。字符是人们使用的记号,一个抽象的符号,这些都是字符:‘1’, ‘中’, ‘a’, ‘$’, ‘¥’ 。
而字节则是计算机中存储数据的单元,一个8位的二进制数。
编码结果中除了标志性的字母b,你还会在编码结果中看到许多\x,你再观察一下这个例子:b’\xce\xe2\xb7\xe3’。
\x是分隔符,用来分隔一个字节和另一个字节。
分隔符还挺常见的,我们在上网的时候,不是会有网址嘛?你经常会看到网址里面有好多的%,它们也是分隔符,替换了Python中的\x。比如像下面这个:
https://www.baidu.com/s?wd=%E5%90%B4%E6%9E%AB
它的意思就是在百度里面,搜索“吴枫”,使用的是UTF-8编码。你眯着眼睛看一看上面的UTF-8编码结果和这一串网址的差异,其实它们除了分隔符以外,是一模一样的。
\xe5\x90\xb4\xe6\x9e\xab # Python编码“吴枫”的结果
%E5%90%B4%E6%9E%AB # 网址里的“吴枫”
此外,用decode()解码的时候则要注意,UTF-8编码的字节就一定要用UTF-8的规则解码,其他编码同理,否则就会出现乱码或者报错的情况,现在请你将下列字节解码成UTF-8的形式,打印出来。
b'\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0'
print(b'\xe6\x88\x91\xe7\x88\xb1\xe4\xbd\xa0'.decode('utf-8'))
运行结果是这样的:
我爱你
所以以后当闷骚的程序猿丢给你这样一串代码时,不要一脸懵,或者你想向不解风情的程序员委婉地表白时,或许可以采用这种清奇的示爱方式噢。
最后我们再来看下ASCII编码,它不支持中文,所以我们来转换一个大写英文字母K。
print('K'.encode('ASCII'))
print(b'K'.decode('ASCII'))
运行结果:
b'K'
K
你看到大写字母K被编码后还是K,但这两个K对计算机来说意义是不同的。前者是字符串,采用系统默认的Unicode编码,占两个字节。后者则是bytes类型的数据,只占一个字节。这也验证我们前面所说的编码就是将str类型转换成bytes类型。
编码知识虽然看起来很琐碎,但它又是非常重要的,如果不能理解这些背景知识,指不定你哪天就会遇到坑,就像隐藏在丛林中的蛇,时不时地咬你一口。而它和我们接下来要教的文件读写也有点关系。
下一篇让我们我们一起学习文件读写。