文本与字节序列
此笔记记录于《流畅的 python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为 jupyter 转的 markdown,原始版 jupyter 笔记在这个仓库
字符问题
Unicode 标准把字符的标识和具体的字节表述进行了如下的明确区分:
- 字符的标识,即码位,是
0~1114111
的数字(十进制),在 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 与 utf-8 之间的关系
s = 'café' # 这里有 4 个 Unicode 字符
len(s) # 4
4
b = s.encode('utf8') # 使用 UTF-8 把 str 编码成 bytes
b
b'caf\xc3\xa9'
len(b) # 字节序列 b 有 5 个字节(在 UTF-8 中,“é”的码位编码成两个字节)
5
b.decode('utf8') # 用 UTF-8 解码成 str
'café'
可以把字节序列想成晦涩难懂的机器磁芯转储,把 Unicode 字符串想成“人类可读”的文本。
字节概要
Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变 bytes 类型和 Python 2.6 添加的可变 bytearray 类型。(Python 2.6 也引入了 bytes 类型,但那只不过是 str 类型的别名,与 Python 3 的 bytes 类型不同。)
bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2 的 str 对象那样是单个的字符。二进制序列的切片始终是同一类型的二进制序列,包括长度为 1 的切片
cafe = bytes('café', encoding='utf_8')
cafe
b'caf\xc3\xa9'
cafe[0] # 99
99
cafe[:1] # b'c'
b'c'
cafe_arr = bytearray(cafe)
cafe_arr
bytearray(b'caf\xc3\xa9')
cafe_arr[-1:] # bytearray(b'é')
bytearray(b'\xa9')
my_bytes[0]
获取的是一个整数,而my_bytes[:1]
返回的是一个长度为 1 的 bytes 对象——这一点应该不会让人意外。s[0]==s[:1]
只对 str 这个序列类型成立。
虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有 ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。
- 可打印的 ASCII 范围内的字节(从空格到~),使用 ASCII 字符本身。
- 制表符、换行符、回车符和\对应的字节,使用转义序列\t、\n、\r 和\。
- 其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)。
我们看到的是b'caf\xc3\xa9'
:前 3 个字节b'caf'
在可打印的 ASCII 范围内,后两个字节则不然。
除了格式化方法(format 和 format_map)和几个处理 Unicode 数据的方法(包括 casefold、isdecimal、isidentifier、isnumeric、isprintable 和 encode)之外,str 类型的其他方法都支持 bytes 和 bytearray 类型
除了格式化方法(format 和 format_map)和几个处理 Unicode 数据的方法(包括 casefold、isdecimal、isidentifier、isnumeric、isprintable 和 encode)之外,str 类型的其他方法都支持 bytes 和 bytearray 类型
bytes.fromhex('31 4B CE A9') # b'1KΩ'
b'1K\xce\xa9'
使用缓冲类对象创建 bytes 或 bytearray 对象时,始终复制源对象中的字节序列。与之相反,memoryview 对象允许在二进制数据结构之间共享内存。
结构体和内存视图:
struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理 bytes、bytearray 和 memoryview 对象。
# 使用 memoryview 和 struct 查看一个 GIF 图像的首部
import struct
fmt = '<3s3sHH' # 结构体的格式:<表示小字节序,3s3s 是两个 3 字节序列,HH 是两个 16 位二进制整数
with open('filter.gif', 'rb') as fp:
img = memoryview(fp.read())
header = img[:10]
bytes(header) # b'GIF89a+\x02\x0f\x00'
struct.unpack(fmt, header) # (b'GIF', b'89a', 555, 15)
del header
del img
基本的编解码器
Python
自带了超过 100 种编解码器(codec, encoder/decoder)
,用于在文本和字节之间相互转换。每个编解码器都有一个名称,如'utf_8',而且经常有几个别名,如'utf8'、'utf-8'和'U8'。这些名称可以传给open( )、
str.encode( )、
bytes.decode( )`等函数的 encoding 参数。
for codec in ['latin_1', 'utf_8', 'utf_16']:
print(codec, 'El Niño'.encode(codec), sep='\t')
latin_1 b'El Ni\xf1o'
utf_8 b'El Ni\xc3\xb1o'
utf_16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
处理编码问题:略(遇到了再说)
有些通信协议和文件格式,如 HTTP 和 XML,包含明确指明内容编码的首部。可以肯定的是,某些字节流不是 ASCII,因为其中包含大于 127 的字节值,而且制定 UTF-8 和 UTF-16 的方式也限制了可用的字节序列。不过即便如此,我们也不能根据特定的位模式来 100%确定二进制文件的编码是 ASCII 或 UTF-8。
然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码。例如,如果b'\x00'
字节经常出现,那么可能是 16 位或 32 位编码,而不是 8 位编码方案,因为纯文本中不能包含空字符;如果字节序列b'\x20\x00'
经常出现,那么可能是 UTF-16LE 编码中的空格字符(U+0020),而不是鲜为人知的 U+2000 EN QUAD 字符
统一字符编码侦测包 Chardet 就是这样工作的,它能识别所支持的 30 种编码。
处理文本文件
处理文本的最佳实践是“Unicode 三明治”
需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开文件时始终应该明确传入 encoding=参数,因为不同的设备使用的默认编码可能不同,有时隔一天也会发生变化。
为了正确比较而规范化 Unicode 字符串
因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。
“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样
s1 = 'café'
s2 = 'cafe\u0301'
s1, s2
('café', 'café')
len(s1), len(s2)
(4, 5)
s1 == s2
False
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 把组合字符分解成基字符和单独的组合字符。
使用 NFKC 和 NFKD 规范化形式时要小心,而且只能在特殊情况中使用,例如搜索和索引,而不能用于持久存储,因为这两种转换会导致数据损失。
大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由str.casefold( )
方法(Python 3.3 新增)支持。对于只包含 latin1 字符的字符串 s,s.casefold( )
得到的结果与s.lower( )
一样,唯有两个例外:微符号'µ'会变成小写的希腊字母“μ”(在多数字体中二者看起来一样);德语Eszett(“sharp s”,ß)
会变成“ss”。
略一些
支持字符串和字节序列的双模式 API
标准库中的一些函数能接受字符串或字节序列为参数,然后根据类型展现不同的行为。re 和 os 模块中就有这样的函数。
如果使用字节序列构建正则表达式,\d
和\w
等模式只能匹配 ASCII 字符;相比之下,如果是字符串模式,就能匹配 ASCII 之外的 Unicode 数字或字母。
import re
re_numbers_str = re.compile(r'\d+') # 字符串类型
re_words_str = re.compile(r'\w+') # 字符串类型
re_numbers_bytes = re.compile(rb'\d+') # 字节序列类型
re_words_bytes = re.compile(rb'\w+') # 字节序列类型
text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef"
" as 1729 = 1³+12³ = 9³+10³.")
text_bytes = text_str.encode('utf_8')
print('Text', repr(text_str), sep='\n ')
print('Numbers')
print(' str :', re_numbers_str.findall(text_str))
print(' bytes:', re_numbers_bytes.findall(text_bytes))
print('Words')
print(' str :', re_words_str.findall(text_str))
print(' bytes:', re_words_bytes.findall(text_bytes))
Text
'Ramanujan saw ௧௭௨௯ as 1729 = 1³+12³ = 9³+10³.'
Numbers
str : ['௧௭௨௯', '1729', '1', '12', '9', '10']
bytes: [b'1729', b'1', b'12', b'9', b'10']
Words
str : ['Ramanujan', 'saw', '௧௭௨௯', 'as', '1729', '1³', '12³', '9³', '10³']
bytes: [b'Ramanujan', b'saw', b'as', b'1729', b'1', b'12', b'9', b'10']
GNU/Linux 内核不理解 Unicode,因此你可能发现了,对任何合理的编码方案来说,在文件名中使用字节序列都是无效的,无法解码成字符串。在不同操作系统中使用各种客户端的文件服务器,在遇到这个问题时尤其容易出错。
为了规避这个问题,os 模块中的所有函数、文件名或路径名参数既能使用字符串,也能使用字节序列。如果这样的函数使用字符串参数调用,该参数会使用 sys.getfilesystemencoding( )得到的编解码器自动编码,然后操作系统会使用相同的编解码器解码。这几乎就是我们想要的行为,与 Unicode 三明治最佳实践一致。
但是,如果必须处理(也可能是修正)那些无法使用上述方式自动处理的文件名,可以把字节序列参数传给 os 模块中的函数,得到字节序列返回值。这一特性允许我们处理任何文件名或路径名,不管里面有多少鬼符