转载请注明出处。请前往 Tiga on Tech 查看原文以及更多有趣的技术文章。
最近用Python写了个简单的爬虫工具,用于爬取Google Play上的游戏类app的信息。在解释及存储爬下来的数据时,为其中的编码问题折腾了一番,于是利用周末时间,好好查了一下资料,了解了一下字符集及字符编码方面的基础知识。为了加深理解并便于日后回顾, 在此将它们记录下来。
1. 字符集和字符编码的概念
字符集:一个系统所支持的所有字符的集合。例如ASCII(American Standard Code for Information Interchange,美国信息交换标准码)字符集,支持的字符包括英文字符、阿拉伯数字等可显示字符,以及回车、换行等控制字符。常见的字符集除了ASCII字符集,还有GB2312字符集、BIG5字符集、Unicode字符集等等。
字符编码:字符集仅仅是一个字符的集合,它并不知道也不关心字符集里的某个字符在计算机上是怎么存储的。计算机怎样存储字符集里的某个字符,是由字符编码来决定的。就是说,字符编码是一个规则,规定某个字符在计算机中怎样被存储和传输。
字符集 vs 字符编码:从上述对两者的描述中可以看出,我们这里讨论的字符集和字符编码是两个完全不同的概念。字符集仅仅是一个字符集合,是一个可以脱离计算机来讨论的概念;而字符编码是和计算机直接相关的,是一套规则,规定字符集里的每一个字符在计算机中是怎样被存储和被传输的。
2. 常见字符集简介
以下将按照出现时间从早到晚,简单介绍几个常见的字符集:
(1) ASCII字符集 & 字符编码
ASCII是最早的一种字符集及字符编码,计算机出现之初,使用的就是ASCII,也是现行最通用的单字节编码系统。
ASCII字符集的基本集包括128个字符,包括现代英语的大小写字母、阿拉伯数字及标点符号等可显示字符,以及空格回车等控制字符;扩展集包括了另外128个字符,包括其他的部分西欧语言使用的字符。因此,整个ASCII字符集定义了共256个字符。在计算机中,使用一个字节(8个bit)即可编码ASCII字符集内的所有字符,其中基本集只使用了一个字节中的低7位。
ASCII支持的所有字符及相应的编码规则,可通过http://www.asciitable.com/进行了解。
(2) GB*字符集 & 字符编码
在介绍具体的字符集前,先介绍一下MBCS(Multi-Byte Character Set),即多字节编码系统。随着计算机在欧美国家之外的地区普及,由于很多地区使用的语言无法用ASCII字符来表示。而由于ASCII的流行,新的字符编码必须与ASCII兼容(与ASCII基本集兼容),因此MBCS设计思想大致为:ASCII基本集中的字符,仍然使用和ASCII字符编码相同的规则,在计算机中,如果第一个字节的值小于0x80("0x"表示"80"是一个16进制的表示),则这个字节表示一个ASCII基本集中的一个字符,如果大于或等于0x80,则和下一个字节一起,两个字节一起表示一个ASCII基本集外的字符,并且在读取下一个字符的时候,跳过第二个字节,再继续往下读取,并按照上述规则继续判断。需要强调的是,MBCS并不是一种特定的字符编码,而是一个统称,统称使用上述规则对字符使用多字节编码的编码规则,包括GB***、BIG***等字符编码。
另外再说一下微软Windows的“ANSI”(记事本->文件->另存为->选择编码方式的时候可以看到),这也不是一种特定的编码方式,而是指“Windows在当前所在地区使用的默认字符编码”,例如在中国,ANSI指代GBK。
现在说回GB***字符集及字符编码。中文里常见的有GB2312和GBK。GB指代GuoBiao(国标),全称中华人民共和国国家标准,2312是标准号。GB2312(具体为GB 2312-80,有时候又称GB或GB0)中文全称为“信息交换用汉字编码字符集 基本集”,通行于中国大陆以及新加坡等地。GB2312字符集涵盖了绝大部分的汉字,但汉字中一些生僻字还是无法使用GB2312来表示和处理。
作为GB2312的扩展,微软利用GB2312中未使用的编码空间,收录了更多的汉字字符,并制定了GBK字符编码。“K”指代的是“扩展”中的“扩”(Kuo)。另外,摘自维基百科汉字内码扩展规范:“根据微软资料,GBK是对GB2312-80的扩展,也就是CP936字码表(Code Page 936)的扩展(之前CP936和GB 2312-80一模一样),最早实现于Windows 95简体中文版。虽然GBK收录GB 13000.1-93的全部字符,但编码方式并不相同。GBK自身并非国家标准,只是曾由国家技术监督局标准化司、电子工业部科技与质量监督司公布为'技术规范指导性文件'。原始GB13000一直未被业界采用,后续国家标准GB18030技术上兼容GBK而非GB13000。”
(3) BIG***字符集 & 字符编码
常见的有BIG5字符集及编码。BIG5是使用繁体中文社区中最常用的电脑汉字字符集标准,普及于港澳台等繁体中文通行区。BIG5仅仅是业界的一个常用标准,并非国家标准。BIG5字符编码属于上述MBCS的一种,使用两个字节来存储一个字符,并且拥有“造字区”供用户(这里的用户指的是计算机/操作系统的生产厂商)自定义字符。例如倚天中文系统、Windows等操作系统都支持BIG5字符集和字符编码,并且定义了自己的造字区,因此BIG5实际上有多个派生的版本。
(4) Unicode字符集及其字符编码
像中文使用的GB2312、BIG5字符集和字符编码一样,很多其他非英文国家和地区,也创造了自己的一套字符集和字符编码。这些字符集和字符编码在当地使用是没有问题的,但由于互联网的发展和普及,使用这些字符集和字符编码的文本文件,一旦通过互联网传播到其他不使用这种字符编码的地区,就变成了乱码了。Unicode,就是为了解决这个问题而被创造出来的。Unicode,用中文可以叫作统一码、万国码等。到目前为止,Unicode字符集定义了超过10个的字符,几乎涵盖了世界上所有国家和地区所使用的字符,并且还在不断地收录新的字符。需要强调的是,Unicode仅仅是一个字符集,而不是字符编码规则,而我们常见的UTF-8、UTF-16和UTF-32等,是Unicode字符集的几种字符编码规则。也就是说,Unicode是一个字符集,而它可以有多种不同的编码方式。Unicode字符集相当于一张很大很大的表格,表格上定义了很多很多字符,每个字符在这张表格上的位置都是固定的,也就是说,每个字符在这个表格上都有一个固定的索引u。UTF-8等字符编码规则,就是定义了Unicode字符集上索引为u的字符,在计算机上是怎样存储和传输的。
首先说UTF-32和UTF-16。UTF-32规定所有的Unicode字符集上的字符都使用32个bit来存储,也就是4个字节。4个字节共能存储2^32次方个不同的字符,超过40亿个,这明显足以编码目前Unicode字符集里的所有字符,并且能保证在很长一段时间内不会因为Unicode字符集的扩张而失效。对于Unicode索引在2^16(65532)以内的字符,UTF-16使用2个字节来存储,而对索引在2^16以外的字符,则使用一些特殊的技巧来处理,这时需要使用更多的字节。UTF-32和UTF-16有明显的优缺点。有点是使用固定字节数来存储一个字符(对于UTF-16,通常假设字符串内的所有字符的Unicode索引都在2^16以内,这已经包含了绝大多数的常用字符了),因此能在常数时间内定位字符串中的第n个字符。而缺点则是,由于计算机存储有大端和小端规则的区别(这个概念在这里不赘述),即假设一个字符的UTF-16编码是0xB2D5,到底高位存储0xB2还是0xD5呢?因此UTF-16等有UTF-16_BE和UTF-16_LE之分。为了解决这个问题,需要在文本文件的开头定义一个BOM(Byte Order Mark)。这是一个特殊的非打印字符,计算机处理一个类似UTF-32/UTF-16这样的多字节编码的文本文件时,根据文件开头的BOM来判断文件是按照大端规则或是小端规则进行存储的。
再来说一下我们最常见的UTF-8编码方式。UTF-8也是Unicode字符集的编码方式之一,可以编码Unicode字符集中的任意字符。所有的互联网协议都支持UTF-8字符编码方式。和UTF-32/UTF-16不同的是,UTF-8是一个可变长度的字符编码方式,它使用1~4个字节来编码一个Unicode字符。另外,UTF-8和ASCII字符编码是兼容的,也就是说,ASCII字符集的字符在使用UTF-8进行编码时,结果和ASCII规则的编码一致,都是使用一个字节实现。上面我们提到,每一个字符在Unicode字符集中都有一个唯一的索引u,我们又把u称作这个字符的Unicode码。有了“Unicode码”的定义后,UTF-8的编码方式可以用下面两条规则简单描述:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
- 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。
上述提到,Unicode是一个字符集,而不是字符编码方式。但我们用Windows的记事本文件另存为时,选择编码方式时能看到“Unicode”和“Unicode big endian”两个选项。这里并不是说“Unicode”是一种编码方式,它其实指代的是“UCS-2”这种字符编码方式,是使用两个字节来编码一个Unicode字符的一种字符编码方式,且是以小端规则进行存储,而“Unicode big endian”则指代这种字符编码方式的大端规则形式。
3. Python 常见字符编码问题
到这里,已经基本介绍完了字符集和字符编码的基础知识。接下来,再总结几个Python中常见的几个字符编码问题。
- 注释头部的“# -- coding: xxxx --”:告诉Python解释器,程序文件中的字符使用的编码方式,以便Python解释器在执行程序时能正确理解其中的字符串。需要注意的是,在使用文本编辑器编写完.py文件后,保存文件时应使用与注释头中声明的编码方式一直的编码方式来保存文件。否则可能导致文件实际是以字符编码A保存的,而Python解释器在执行时却以字符编码B的规则来解释其中的字符寸,导致Python程序产生非预期的结果。
- unicode类和str类:简单来说,unicode是“字符串”,而str是“字节寸”。以汉字“大”字为例,声明一个“大”字的unicode对象的方法为“u = u'大'”,声明str对象的方法为“s = '大'”。声明str对象时,Python会根据文件头部的coding注释项来对该字符寸进行编码,得到的字节串就是str的值。假如对u使用len()函数获取其长度,结果为1,假设默认编码方式(文件头部的coding注释)为UTF-8,因为UTF-8使用3个字节来存储中文“大”字,因此对s使用len()函数,得到的结果为3。这能说明,unicode是“字符串”,而str是“字节串”这个理解是合适的。
- encode() vs decode():对unicode对象使用encode()方法,可将unicode字符按照某种字符编码规则进行编码,并返回编码后的str对象。相反,对str对象使用decode()方法,可将一个字节串按照某种字符编码规则进行解码,得到相应的Unicode字符,并返回解码后的unicode对象。
- 文件读写:使用内置的open()函数打开文件,并用read()方法读取,读取到的是str对象,即一个字节串,可使用decode方法得到相应的unicode字符串。使用write()方法写文件时,如果参数是str对象,则直接写入字节串,如果时unicode对象,则会使用.py文件头部的coding注释声明的字符编码对unicode对象进行encode编码操作,然后再写入相应的字节串。
- decode('unicode-escape'):这个常常令人迷惑。举例子:
'u5927'.decode('unicode-escape')
相当于取 unicode 字符集的第 5927(16进制) 号字符(https://unicode-table.com/en/#5927),刚好是中文字符“大”,返回的结果是 unicode 类型的字符。