背景:
文本以及字符串处理在网络应用中随处可见,面对不同的用户接口调用方,以及不同的我们需要调用的接口,可能都会有不一样的编码规范。
而我在以前的开发中,对于字符串的处理经常会是以猜的方式来处理,encode、decode等各种方式都试一遍,发现哪个能解决问题就用哪个,从来没有真正的了解字符串相关的原理与逻辑。
所以,最近通过阅读《Fluent Python》一书以及一些源码、博文,好好了解了一下字符相关的原理以及在开发中的应用,记录一下学习过程中觉得有用的地方以及一些理解。
ASCII,Unicode 和 UTF-8之间的关系
ASCII码
ASCII是最早的编码,美国发明,使用一个字节8位,最多可以展示255个字符,但是ASCII编码中第一位没有使用,仅用字节的后七位表示了127个字符。
Unicode
Unicode是一种所有符号的编码,包含了世界上各种语言的符号,目前有一百多万个符号在这个集合中。
但是Unicode只是一个集合,是把所有的字符就放在这个集合里,它规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
因为Unicode表示的字符大部分需要两个字节,甚至有的会更多。如果没有规定的存储方式,或者合适的存储方式,统一用两个字节来表示所有的字符,那么ASCII码中本来可以用一个字节表示的英文字母,就也需要两个字节表示(第一个字节全部是0),这样如果目标全是英文,就相当于占用的存储空间直接翻倍了。
UTF-8
UTF-8(UTF-16)是 Unicode 的实现方式之一。UTF-8是目前使用最广的一种 Unicode 的实现方式。
UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,英文字母被编码成1个字节,汉字通常是3个字节,少量很生僻的字符会被编码成4-6个字节。这样在全是英文的地方,就不会用浪费空间,对于需要多字节的汉字,也能正常展示。
UTF-8的编码规则有一下两条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
如上图所示为Unicode符号与UTF-8的对应关系举例。
识别UTF-8有多少字节的核心是:如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
因为UTF-8在只使用一个字节的时候,和ASCII是一样的,所以UTF-8是可以兼容ASCII的。
综上所述,有如下结论:
Unicode和ASCII是同一种概念,是是一种编码方式。而UTF-8,UTF-16等是一种存储方式,在存储和传输上节约空间、提高性能的一种编码形式。
字符相关在Python中的使用
《Fluent Python》:
Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分。
字符的标识,即码位,是0~1 114 111的数字(十进制),在Unicode 标准中以4~6 个十六进制数字表示,而且加前缀“U+”。例如,字母A的码位是U+0041,欧元符号的码位是U+20AC,高音谱号的码位是U+1D11E。在Unicode 6.3中(这是Python 3.4使用的标准),约10%的有效码位有对应的字符。
字符的具体表述取决于所用的编码。编码是在码位和字节序列之间转换时使用的算法。在UTF-8编码中,A(U+0041)的码位编码成单个字节\x41,而在UTF-16LE编码中编码成两个字节\x41\x00。再举个例子,欧元符号(U+20AC)在UTF-8编码中是三个字节——\xe2\x82\xac,而在UTF-16LE中编码成两个字节:\xac\x20。
把码位转换成字节序列的过程是编码,把字节序列转换成码位的过程是解码。通俗点讲,在开发过程中会碰到两种字符:存储在机器中的是字节序列,展现出来是Unicode字符(人类可读的文本)。将机器读取的文本转为人类可读的文本就是解码(decode),将人类可读的文本转为给机器读取的文本就是编码(encode)。
在我们项目中的应用
在开发过程中,为了避免对接不同的接口或者平台的时候出现编码方式不同造成的报错,我们需要在整个系统中都统一同一套编码方式。
我们使用tornado框架,框架中get_argument方法默认会将拿到的参数转为unicode:
在我们项目的utils.py文件中,一般也都会有针对字符串的编码转换的方法可以调用:
在编写脚本的时候,在文件顶部定义脚本的编码方式:
在具体使用的地方,也要根据具体需求将文本解码为我们所使用的UTF-8。
Python2规范化字符编码
Python2(从2.5开始)则默认使用ASCII。因此直接创建的字符串一般默认为ASCII,如果调用接口或者进行需要UTF-8的处理,需要对字符串进行解码。
在进行字符串处理的时候,可能会遇到无法编码或者解码的报错,要根据具体情况处理。
一般比较常见的是两种错误:UnicodeEncodeError(把字符串转换成二进制序列时),UnicodeDecodeError(把二进制序列转换成字符串时)。这种情况是因为目标编码方式没有没有我们需要编码/解码的字符串中的某个字符或者符号。在进行测试或者自己的一些简单开发的时候,可以在使用encode的时候,增加对errors的指定处理方式来跳过(实际开发中,需要让异常正常抛出):
city.encode('cp437', errors='ignore') 跳过错误。
city.encode('cp437', errors='replace') 将无法编码的字符用?代替。
city.encode('cp437', errors='xmlcharrefreplace')把无法编码的字符替换成XML实体。
对于有些情况,还可能出现音字符的情况,对于同样一个字符,可能会有不同的Unicode编码方式。如下图:
此时,可以使用unicodedata.normalize函数对字符串进行Unicode规范化。
这个函数的第一个参数是这4个字符串中的一个:'NFC'、'NFD'、'NFKC'和'NFKD'。我们可以主要使用前两个:NFC(Normalization Form C)使用最少的码位构成等价的字符串,NFD把组合字符分解成基字符和单独的组合字符。具体使用如下:
unicodedata中的combining()函数可以用来判断一个字符是否是音字符,可以用来将文本中的音字符全部过滤掉。
在处理字符串的时候,还有一些方法可以直接对字符串进行简单判断,灵活运用起来会大大提高开发效率。包括isdigital(), startswith(),endswith()等。