1. 文本和编码
字符串是个相当简单的概念:一个字符串是一个字符序列.问题出在"字符"的定义上.
在2015 年,"字符"的最佳定义是Unicode
字符.因此从Python 3的str
对象中获取的元素是Unicode
字符
Unicode标准把字符的标识和具体的字节表述进行了如下的明确区分.
字符的标识,即码位是
0~1 114 111
的数字(十进制).在Unicode
标准中以4~6
个十六进制数字表示,而且加前缀U+
.例如字母A
的码位是U+0041
,欧元符号的码位是U+20AC
,高音谱号的码位是U+1D11E
.在
Unicode 6.3
标准中约10%的有效码位有对应的字符.字符的具体表述取决于所用的编码.编码是在码位和字节序列之间转换时使用的算法.在
UTF-8
编码中,A(U+0041)
的码位编码成单个字节\x41
,而在UTF-16LE
编码中编码成两个字节\x41\x00
.再举个例子:欧元符号(U+20AC)
在UTF-8
编码中是三个字节--\xe2\x82\xac
,而在UTF-16LE
中编码成两个字节--\xac\x20
.
把码位转换成字节序列的过程是编码,使用encode
;把字节序列转换成码位的过程是解码,使用decode
.
非英语用户常常会搞反所谓的编码解码,可以这样理解--把Unicode
字符串想成"人类可读"的文本.那么
- 把字节序列变成人类可读的文本字符串就是解码
- 而把字符串变成用于存储或传输的字节序列就是编码
s = "中文文本" len(s)
4
b = s.encode("utf-8")#编码 b
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
len(b)
12
b.decode("utf-8")
'中文文本'
1.1. 混乱的编码问题
现今使用UTF-8编码是最通用的,但编码存在很多"历史遗留问题",比如中文编码混乱的问题(非英语都有这个问题)
chardet是一个用于推断编码类型的工具,可以使用pip安装,使用它可以大致判断出文本使用的是什么编码,并给出该编码的可能性大小.具体用法可以看它的文档,下面是最简单的用法
import chardet
chardet.detect(b)
4
0
4
1
4
2
1.2. 基本的编解码器
Python自带了超过100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换.每个编解码器都有一个名称,如utf_8
,而且经常有几个别名,如utf8
、utf-8
和U8
.这些名称可以传给open()
、str.encode()
、bytes.decode()
等函数的encoding
参数.
如下是几种最常见的编码:
- latin1(即iso8859_1)
一种重要的编码,是其他编码的基础,例如cp1252和Unicode(注意latin1
与cp1252
的字节值是一样的甚至连码位也相同)
- cp1252
微软制定的latin1
超集,也是windows下各种编码问题的万恶之源,相对于latin1
添加了有用的符号,例如弯引号和€(欧元);有些Windows 应用把它称为ANSI
,但它并不是ANSI标准.
- cp437
IBM PC 最初的字符集,包含框图符号.与后来出现的latin1
不兼容.
- gb2312
用于编码简体中文的陈旧标准,这是亚洲语言中使用较广泛的多字节编码之一.网络上中文乱码的万恶之源之一
- utf-8
目前Web中最常见的8位编码;与ASCII
兼容(纯ASCII
文本是有效的UTF-8
文本)
- utf-16le
UTF-16
的16位编码方案的一种形式;所有UTF-16
支持通过转义序列(称为"代理对",surrogate pair)表示超过U+FFFF
的码位. UTF-16
取代了1996年发布的Unicode 1.0
编码(UCS-2).这个编码在很多系统中仍在使用,但是支持的最大码位是U+FFFF
.从Unicode 6.3
起,分配的码位中有超过50%在U+10000
以上,包括逐渐流行的表情符号(emoji pictograph
).
4
3
4
4
1.2.1. 大字节序还是小字节序
你可能注意到了,UTF-16
编码的序列开头有几个额外的字节\xff\xfe
这是BOM,即字节序标记(byte-order mark),指明编码时使用Intel CPU的小字节序.
在小字节序设备中,各个码位的最低有效字节在前面:字母'E'的码位是U+0045(十进制数69),在字节偏移的第2位和第3位编码为69和0.
在大字节序CPU中,编码顺序是相反的;'E'编码为0和69.
为了避免混淆,UTF-16
编码在要编码的文本前面加上特殊的不可见字符ZERO WIDTH NOBREAK SPACE(U+FEFF)
.在小字节序系统中,这个字符编码为b'\xff\xfe'
(十进制数 255, 254).因为按照设计,U+FFFE
字符不存在,在小字节序编码中,字节序列b'\xff\xfe'
必定是ZERO WIDTH NO-BREAK SPACE
,所以编解码器知道该用哪个字节序.
UTF-16
有两个变种:
UTF-16LE
,显式指明使用小字节序;UTF-16BE
,显式指明使用大字节序.
如果使用这两个变种,不会生成BOM.
与字节序有关的问题只对一个字(word)
占多个字节的编码(如UTF-16
和UTF-32
)有影响.UTF-8
的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要BOM.尽管如此,某些Windows应用(尤其是Notepad)依然会在UTF-8
编码的文件中添加BOM;而且Excel会根据有没有BOM确定文件是不是UTF-8
编码,否则它假设内容使用Windows代码页(codepage)编码.UTF-8
编码的U+FEFF
字符是一个三字节序列:b'\xef\xbb\xbf'
.因此,如果文件以这三个字节开头,有可能是带有BOM的UTF-8
文件.然而,Python
不会因为文件以b'\xef\xbb\xbf'
开头就自动假定它是UTF-8编码的.
1.3. ascii码支持
binascii
是处理ascii编码的工具,binascii模块包含很多在二进制和ASCII编码的二进制表示转换的方法.通常情况不会直接使用这些功能,而是使用像UU,base64编码,或BinHex封装模块. binascii模块包含更高级别的模块使用的,用C语言编写的低级高效功能.
接口如下:
binascii.a2b_uu(string)
将一行uuencoded数据转换回二进制并返回二进制数据.线通常包含45(二进制)字节,除了最后一行.行数据后面可以是空格.
binascii.b2a_uu(data)
将二进制数据转换成一行ASCII字符,返回值是转换行,包括换行符.数据长度最多为45.
binascii.a2b_base64(string)
将一个base64数据块转换回二进制数据并返回二进制数据.一次可能会传递多条线.
binascii.b2a_base64(data, *, newline=True)
将二进制数据转换为base64编码中的ASCII字符行.返回值是转换行,如果换行符为true则包含换行符.此功能的输出符合RFC 3548.
binascii.a2b_qp(data, header=False)
将可打印数据块转换回二进制数据并返回二进制数据.一次可能会传递多条线.如果可选参数头存在且为真,下划线将被解码为空格.
binascii.b2a_qp(data, quotetabs=False, istext=True, header=False)
将二进制数据转换成可引用可打印编码的ASCII字符行.返回值是转换行.如果可选参数quotetabs存在且为真,则将对所有选项卡和空格进行编码.如果可选参数istext存在且为真,那么换行不会被编码,而尾随空白将被编码.如果可选参数头存在且为真,则空格将按照RFC1522编码为下划线.如果可选参数头存在且为false,则换行符也将被编码;否则换行转换可能会损坏二进制数据流.
binascii.a2b_hqx(string)
将binhex4格式的ASCII数据转换为二进制,无需进行RLE解压缩.字符串应包含完整数量的二进制字节,或(在binhex4数据的最后一部分的情况下)剩余的位为零.
binascii.rledecode_hqx(data)
根据binhex4标准对数据执行RLE解压缩.该算法在一个字节后使用
0x90
作为重复指示符,后跟计数.计数为0指定字节值0x90
.例程返回解压缩的数据,除非数据输入数据在孤立的重复指示符中结束,在这种情况下会引发Incomplete
异常.
binascii.rlecode_hqx(data)
对数据执行binhex4风格的RLE压缩并返回结果
binascii.b2a_hqx(data)
执行hexbin4二进制到ASCII转换并返回生成的字符串.该参数应该已经是RLE编码,并且可以将长度除以3(除了可能的最后一个片段)
binascii.crc_hqx(data, value)
计算数据的16位CRC值,以值作为初始CRC开始,并返回结果.这使用CRC-CCITT多项式x16 x12 x5 1,通常表示为
0x1021
.该CRC用于binhex4格式.binascii.crc32(data[, value])
计算CRC-32,数据的32位校验和,以值的初始CRC开始.默认的初始CRC为零.该算法与ZIP文件校验和一致.由于该算法被设计为用作校验和算法,因此不适合用作通用散列算法.使用如下:
4
5
4
6
binascii.b2a_hex(data)
binascii.hexlify(data)
返回二进制数据的十六进制表示.数据的每个字节被转换成相应的2位十六进制表示.因此返回的字节对象是数据长度的两倍
binascii.a2b_hex(hexstr)
binascii.unhexlify(hexstr)
返回由十六进制字符串hexstr表示的二进制数据,这个函数是
b2a_hex()
的倒数.hexstr必须包含偶数个十六进制数字(可以是大写或小写),否则会出现错误异常.
1.4. python3支持Unicode
python3从头到脚都支持Unicode,这也意味着像java一样,你可以将类名,函数名,变量名都设为中文(或者其他语言).不少人认为这样做不好,但考虑到代码的传播范围,其实使用更加便于交流的文字是更好的方法.比如,这个代码是一个日本企业内部使用的,而且他们并不打算让外国人用也不打算向前兼容python2,那么他们完全可以使用全日语来写文档,定义变量,函数,类.
1.5. 为了正确比较而规范化Unicode字符串
因为Unicode
有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以拉丁语系文字比如法语,意大利语字符串比较起来很复杂,我们拿café
这个词来作为例子.这个词可以使用两种方式构成,分别有4个和5个码位,但是结果完全一样.
4
7
4
8
4
9
b = s.encode("utf-8")#编码 b
0
b = s.encode("utf-8")#编码 b
1
b = s.encode("utf-8")#编码 b
2
b = s.encode("utf-8")#编码 b
3
b = s.encode("utf-8")#编码 b
4
U+0301
是COMBINING ACUTE ACCENT
,加在e
后面得到é
.在Unicode
标准中,é
和e\u0301
这样的序列叫"标准等价物"(canonical equivalent),应用程序应该把它们视作相同的字符.但是,Python看到的是不同的码位序列,因此判定二者不相等.
这个问题的解决方案是使用标准库的unicodedata.normalize
函数提供的Unicode规范化.这个函数的第一个参数是这4个字符串中的一个:'NFC'、'NFD'、'NFKC' 和'NFKD'
- NFC(Normalization Form C)和NFD (Normalization Form D)
使用最少的码位构成等价的字符串,而NFD 把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期:
b = s.encode("utf-8")#编码 b
5
b = s.encode("utf-8")#编码 b
6
b = s.encode("utf-8")#编码 b
7
b = s.encode("utf-8")#编码 b
8
b = s.encode("utf-8")#编码 b
9
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
0
西方键盘通常能输出组合字符,因此用户输入的文本默认是NFC形式.不过,安全起见,保存文本之前,最好使用normalize('NFC', user_text)清洗字符串.NFC也是W3C的Character Model for the World Wide Web: String Matching and Searching
规范推荐的规范化形式。
- NFKC和NFKD
在另外两个规范化形式(NFKC 和NFKD)的首字母缩略词中,字母K表示"compatibility"(兼容性).这两种是较严格的规范化形式,对"兼容字符"有影响.虽然Unicode
的目标是为各个字符提供"规范的"码位,但是为了兼容现有的标准,有些字符会出现多次.例如虽然希腊字母表中有"μ"这个字母(码位是U+03BC
,GREEK SMALL LETTER MU
),但是Unicode
还是加入了微符号μ
(U+00B5
)以便与latin1
相互转换.因此,微符号是一个"兼容字符".
在NFKC和NFKD形式中,各个兼容字符会被替换成一个或多个"兼容分解"字符,即便这样有些格式损失,但仍是"首选"表述--理想情况下格式化是外部标记的职责,不应该由Unicode处理.下面举个例子:
二分之一
½
(U+00BD
)经过兼容分解后得到的是三个字符序列'1/2';微符号μ
(U+00B5
)经过兼容分解后得到的是小写字母μ
(U+03BC
)
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
1
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
2
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
3
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
4
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
5
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
6
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
7
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
8
b'\xe4\xb8\xad\xe6\x96\x87\xe6\x96\x87\xe6\x9c\xac'
9
len(b)
0
使用1/2
替代½
可以接受,微符号也确实是小写的希腊字母μ
,但是把4²
转换成42
就改变原意了.某些应用程序可以把4²
保存为4<sup>2</sup>
,但是normalize函数对格式一无所知.因此NFKC 或NFKD可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述--用户搜索1 ⁄ 2 inch
时如果还能找到包含½ inch
的文档那么用户会感到满意.
使用NFKC和NFKD规范化形式时要小心,而且只能在特殊情况中使用,例如搜索和索引,而不能用于持久存储,因为这两种转换会导致数据损失.
1.5.1. 大小写折叠
大小写折叠其实就是把所有文本变成小写,再做些其他转换.这个功能由str.casefold()
实现.
对于只包含latin1
字符的字符串s,s.casefold()
得到的结果与s.lower()
一样,唯有两个例外:
- 微符号
μ
会变成小写的希腊字母μ
(在多数字体中二者看起来一样); - 德语
Eszett("sharp s",ß)
会变成ss
自Python 3.4 起,str.casefold()
和str.lower()
得到不同结果的有116个码位。Unicode6.3
命名了110122个字符,这只占0.11%
1.5.2. 极端"规范化":去掉变音符号
去掉变音符号不是正确的规范化方式,因为这往往会改变词的意思,而且可能误判搜索结果.但是对现实生活却有所帮助--人们有时很懒或者不知道怎么正确使用变音符号,而且拼写规则会随时间变化.因此实际语言中的重音经常变来变去.
比如café
,对于中国人来说é
很难打出来,所以用户往往就打cafe
了,我们需要一个去掉组合记号的函数用来实现这种极端的规范化
len(b)
1
len(b)
2
len(b)
3
len(b)
4
len(b)
5
len(b)
6
len(b)
7
1.5.3. 总结unicode规范化:
- NFC和NFD可以放心使用,而且能合理比较Unicode字符串
- 对大多数应用来说,NFC是最好的规范化形式
- 不区分大小写的比较应该使用
str.casefold()
- 在必要的时候,我们可以删除一些变音符号来做规范化
1.6. 文本排序
Python比较任何类型的序列时,会一一比较序列里的各个元素.对字符串来说,比较的是码位.可是在比较非ASCII字符时,得到的结果不尽如人意.
len(b)
8
len(b)
9
12
0
按照中文传统,我们应该希望按拼音首字母顺序排序即后前右左,但明显不是.
实际上在Python中,非ASCII文本的标准排序方式是使用locale.strxfrm
函数,根据locale
模块的文档,这个函数会"把字符串转换成适合所在区域进行比较的形式".使用locale.strxfrm
函数之前,必须先为应用设定合适的区域设置,还要祈祷操作系统支持这项设置.在区域设为pt_BR
的GNU/Linux(Ubuntu 14.04)
中.而在windows中还没这个功能.
在Linux操作系统中中国大陆的读者可以使用zh_CN.UTF-8
,简体中文会按照汉语拼音顺序进行排序.
1.6.1. unicode排序工具PyUCA
James Tauber,一位高产的Django
贡献者,他一定是感受到了这一痛点,因此开发了 PyUCA 库,这是Unicode排序算法包.
PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给Collator()
构造方法。PyUCA 默认使用项目自带的allkeys.txt
,这就是Unicode 6.3.0
的"Default Unicode Collation Element Table"的副本
12
1
12
2
1.7. 以指定列宽格式化字符串
1.8. 文本标注
python定义字符串使用成对的'
,"
,"""
,'''
构建而成,而文本可以标注为r
,u
,b
,f
等
12
3
12
4
12
5
1.8.1. 原始文本标注r
用r
标注的文本表示不会理会转义字符\
12
6
12
4
12
8
1.8.2. unicode文本标注u
用u
标注的文本表示字符串为unicode,python3中str就是Unicode,所以其实这个没什么意义,主要是为了给python3向前兼容的
1.8.3. btypes文本标注b
用b标注的文本标识文本为bytes,使用这个标注说明文本是字节序列
1.8.4. 格式化文本标注f
用f标注的文本表示其中有用{}
占位的内容由前面定义的变量填充,需要注意一旦标注为f则文本实际上已经不是文本了,而是一个函数,如果占位符没有找到对应的变量,则会报错,将f标注的文本放入函数中指定参数为其中的占位符也没有用
12
9
b.decode("utf-8")
0
b.decode("utf-8")
1
1.9. 格式化字符串
python可以使用str.format()
方法来格式化字符串这种方式更加直观
b.decode("utf-8")
2
b.decode("utf-8")
3
b.decode("utf-8")
4
b.decode("utf-8")
3
1.10. 文本模板
python提供了一个模块from string import Template
,可以用于定义文本模板.它通常用来作为文件的内容模板.
Template用起来和字符串的format方法类似,使用$
标识要替换的占位字符,然后使用方法substitute
来替换.以下是一个dockerfile的文本模板
b.decode("utf-8")
6
b.decode("utf-8")
7
b.decode("utf-8")
8
b.decode("utf-8")
9
'中文文本'
0
还没有评论,来说两句吧...