四.文本和字节序列

字节概要

bytes 或 bytearray 对象的各个元素是介于 0~255之间的整数,
>>> cafe = bytes('café', encoding='utf_8') ➊ 
>>> cafe b'caf\xc3\xa9' >>> cafe[0] ➋ 
99
>>> cafe[:1] ➌ 
b'c' 
>>> cafe_arr = bytearray(cafe) 
>>> cafe_arr ➍ 
bytearray(b'caf\xc3\xa9') 
>>> cafe_arr[-1:] ➎ 
bytearray(b'\xa9')
❶ bytes 对象可以从 str 对象使用给定的编码构建。
❷ 各个元素是 range(256) 内的整数。
❸ bytes 对象的切片还是 bytes 对象,即使是只有一个字节的切片。
❹ bytearray 对象没有字面量句法,而是以 bytearray() 和字节序列
字面量参数的形式显示。
❺ bytearray 对象的切片还是 bytearray 对象。
 
虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。
 
1.可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身。
2.制表符、换行符、回车符和 \ 对应的字节,使用转义序列\t、\n、\r 和 \\。
3.其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)。
 
因此,我们看到上面的是 b'caf\xc3\xa9':前 3 个字节b'caf' 在可打印的 ASCII 范围内,后两个字节则不然。
 
结构体和内存视图
struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理 bytes、bytearray memoryview 对象。
memoryview 类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列。
 
使用 memoryview 和 struct 查看一个 GIF 图像的首部
>>> import struct 
>>> fmt = '<3s3sHH' # ➊ 
>>> with open('filter.gif', 'rb') as fp:
...     img = memoryview(fp.read()) #... 
>>> header = img[:10] # ➌ 
>>> bytes(header) # ➍ 
b'GIF89a+\x02\xe6\x00' 
>>> struct.unpack(fmt, header) # ➎ 
(b'GIF', b'89a', 555, 230) 
>>> del header # ➏ 
>>> del img
❶ 结构体的格式:< 是小字节序,3s3s 是两个 3 字节序列,HH 是两个16 位二进制整数。
❷ 使用内存中的文件内容创建一个 memoryview 对象……
❸然后使用它的切片再创建一个 memoryview 对象;这里不会复制字节序列。
❹ 转换成字节序列,这只是为了显示;这里复制了 10 字节。
❺ 拆包 memoryview 对象,得到一个元组,包含类型、版本、宽度和高度。
❻ 删除引用,释放 memoryview 实例所占的内存。
 

基本的编解码器

Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。每个编解码器都有一个名称,如 'utf_8',而且经常有几个别名,如 'utf8'、'utf-8' 和 'U8'。这些名称可以传给 open()、str.encode()、bytes.decode() 等函数的 encoding 参数。
 
使用 3 个编解码器编码字符串“El Niño”,得到的字节序列差异很大
>>> 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'

 

了解编码问题

处理UnicodeEncodeError
多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。

处理UnicodeDecodeError
不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是有效的 UTF-8 或 UTF-16。因此,把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出UnicodeDecodeError。
 
使用预期之外的编码加载模块时抛出的SyntaxError
Python 3 默认使用 UTF-8 编码源码,Python 2(从 2.5 开始)则默认使用ASCII。如果加载的 .py 模块中包含 UTF-8 之外的数据,而且没有声明编码,会得到类似下面的消息:
SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line 1, 
but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

 

处理文本文件

处理文本的最佳实践是“Unicode 三明治”(如图 4-2 所示)。4 意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串;Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。

在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要的编码,所以调用 my_file.read() 方法得到的以及传给my_file.write(text) 方法的都是字符串对象。

处理文本文件很简单。但是,如果依赖默认编码,你会遇到麻烦。

一个平台上的编码问题(如果在你的机器上运行,它可能会发生,也可能不会)
>>> open('cafe.txt', 'w', encoding='utf_8').write('café') 
4
>>> open('cafe.txt').read()
'café'
问题是:写入文件时指定了 UTF-8 编码,但是读取文件时没有这么做,因此 Python 假定要使用系统默认的编码(Windows 1252),于是文件的最后一个字节解码成了字符 'é',而不是 'é'。

为了正确比较而规范化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 把组合字符分解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期:
>>> from unicodedata import normalize 
>>> s1 = 'café' # 把"e"和重音符组合在一起 
>>> s2 = 'cafe\u0301' # 分解成"e"和重音符 
>>> len(s1), len(s2) 
(4, 5) 
>>> len(normalize('NFC', s1)), len(normalize('NFC', s2))
(4, 4) 
>>> len(normalize('NFD', s1)), len(normalize('NFD', s2)) 
(5, 5) 
>>> normalize('NFC', s1) == normalize('NFC', s2) 
True 
>>> normalize('NFD', s1) == normalize('NFD', s2) 
True
 
在 NFKC 和 NFKD 形式中,各个兼容字符会被替换成一个或多个“兼容分解”字符,即便这样有些格式损失,但仍是“首选”表述——理想情况下,格式化是外部标记的职责,不应该由 Unicode 处理。
下面举个例子,二分之一 '½'(U+00BD)经过兼容分解后得到的是三个字符序列'1/2';微符号 'µ'(U+00B5)经过兼容分解后得到的是小写字母'μ'(U+03BC)。
 
下面是 NFKC 的具体应用:
>>> from unicodedata import normalize, name 
>>> half = '½' 
>>> normalize('NFKC', half) 
'1⁄2' 
>>> four_squared = '' 
>>> normalize('NFKC', four_squared) 
'42' 
>>> micro = 'μ' 
>>> micro_kc = normalize('NFKC', micro) 
>>> micro, micro_kc 
('μ', 'μ') 
>>> ord(micro), ord(micro_kc) 
(181, 956) 
>>> name(micro), name(micro_kc) 
('MICRO SIGN', 'GREEK SMALL LETTER MU')
使用 '1/2' 替代 '½' 可以接受,微符号也确实是小写的希腊字母'µ',但是把 '4²' 转换成 '42' 就改变原意了。某些应用程序可以把'4²' 保存为 '4<sup>2</sup>',但是 normalize 函数对格式一无所知。因此,NFKC 或 NFKD 可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述:用户搜索 '1 / 2 inch' 时,如果还能找到包含 '½ inch' 的文档,那么用户会感到满意。
 
大小写折叠
大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold() 方法(Python 3.3 新增)支持。
对于只包含 latin1 字符的字符串 s,s.casefold() 得到的结果与s.lower() 一样,唯有两个例外:微符号 'µ' 会变成小写的希腊字母“μ”(在多数字体中二者看起来一样);德语 Eszett(“sharp s”,ß)会变成“ss”。
>>> micro = 'μ' 
>>> name(micro) 
'MICRO SIGN' 
>>> micro_cf = micro.casefold() 
>>> name(micro_cf) 
'GREEK SMALL LETTER MU' 
>>> micro, micro_cf 
('μ', 'μ') 
>>> eszett = 'ß' 
>>> name(eszett) 
'LATIN SMALL LETTER SHARP S' 
>>> eszett_cf = eszett.casefold() 
>>> eszett, eszett_cf 
('ß', 'ss')

 

规范化文本匹配实用函数
由前文可知,NFC 和 NFD 可以放心使用,而且能合理比较 Unicode 字符串。对大多数应用来说,NFC 是最好的规范化形式。不区分大小写的比较应该使用 str.casefold()。
如果要处理多语言文本,工具箱中应该有nfc_equal 和fold_equal 函数。
 
极端“规范化”:去掉变音符号
 
 

Unicode文本排序

Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽如人意。
下面对一个生长在巴西的水果的列表进行排序:
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] 
>>> sorted(fruits) 
['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
不同的区域采用的排序规则有所不同,葡萄牙语等很多语言按照拉丁字母表排序,重音符号和下加符对排序几乎没什么影响。9 因此,排序时“cajá”视作“caja”,必定排在“caju”前面。
排序后的 fruits 列表应该是:
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

 

使用Unicode排序算法排序
使用 pyuca.Collator.sort_key 方法
 
>>> import pyuca 
>>> coll = pyuca.Collator() 
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] 
>>> sorted_fruits = sorted(fruits, key=coll.sort_key) 
>>> sorted_fruits 
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

 

Unicode数据库

Unicode 标准提供了一个完整的数据库(许多格式化的文本文件),不仅包括码位与字符名称之间的映射,还有各个字符的元数据,以及字符之间的关系。字符串的isidentifier、isprintable、isdecimal 和 isnumeric 等方法就是靠这些信息作判断的。 str.casefold 方法也用到了 Unicode 表中的信息。
unicodedata 模块中有几个函数用于获取字符的元数据。示例展示了unicodedata.name() 和 unicodedata.numeric() 函数,以及字符串的 .isdecimal() 和 .isnumeric() 方法的用法。
import unicodedata 
import re 

re_digit = re.compile(r'\d') 
sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285' 
for char in sample: 
    print('U+%04x' % ord(char), ➊ 
    char.center(6), ➋ 
    're_dig' if re_digit.match(char) else '-', ➌ 
    'isdig' if char.isdigit() else '-', ➍ 
    'isnum' if char.isnumeric() else '-', ➎ 
    format(unicodedata.numeric(char), '5.2f'), ➏ 
    unicodedata.name(char), ➐ 
    sep='\t')    

➊ U+0000 格式的码位。
➋ 在长度为 6 的字符串中居中显示字符。
➌ 如果字符匹配正则表达式 r'\d',显示 re_dig。
➍ 如果 char.isdigit() 返回 True,显示 isdig。
➎ 如果 char.isnumeric() 返回 True,显示 isnum。
➏ 使用长度为 5、小数点后保留 2 位的浮点数显示数值。
➐ Unicode 标准中字符的名称。
 
posted @ 2019-09-20 20:14  tianqibucuo  阅读(509)  评论(0)    收藏  举报