第4章 文本和字节序列
# 《流畅的Python》读书笔记
# 第4章 文本和字节序列 # 人类使用文本,计算机使用字节序列。 # 深入理解 Unicode 对你可能十分重要,也可能无关紧要,这取决于Python 编程的场景。 #本章将讨论下述话题: #字符、码位和字节表述 #bytes、bytearray 和 memoryview 等二进制序列的独特特性 #全部 Unicode 和陈旧字符集的编解码器 #避免和处理编码错误 #处理文本文件的最佳实践 #默认编码的陷阱和标准 I/O 的问题 #规范化 Unicode 文本,进行安全的比较 #规范化、大小写折叠和暴力移除音调符号的实用函数 #使用 locale 模块和 PyUCA 库正确地排序 Unicode 文本 #Unicode 数据库中的字符元数据 #能处理字符串和字节序列的双模式 API # 4.1 字符问题 # “字符串”是个相当简单的概念:一个字符串是一个字符序列。 # 如果想帮助自己记住.decode()和.encode()的区别,可以把字节序列想成晦涩难懂的机器磁芯转储,把Unicode字符串想成“人类可读”的文本。 # 那么,把字节序列变成人类可读的文本字符串就是解码,而把字符串变成用于存储或传输的字节序列就是编码。 # 示例 4-1 编码和解码 s = 'café' print(len(s)) #4 b = s.encode('utf8') print(b) #b'caf\xc3\xa9' print(len(b)) #5 print(b.decode('utf8')) #café # 4.2 字节概要 # bytes 或 bytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2 的 str 对象那样是单个的字符。 # 示例 4-2 包含 5 个字节的 bytes 和 bytearray 对象 # 示例 4-3 使用数组中的原始数据初始化bytes对象 import array numbers=array.array('h',[-2,-1,0,1,2]) octets=bytes(numbers) print(octets) #b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' # 示例 4-4 使用memoryview和struct查看一个GIF图像的首部 import struct fmt='<3s3sHH' with open('2.gif','rb') as fp: img=memoryview(fp.read()) header=img[:10] print(bytes(header)) #b'\xff\xd8\xff\xe0\x00\x10JFIF' print(struct.unpack(fmt, header)) #(b'\xff\xd8\xff', b'\xe0\x00\x10', 17994, 17993) del header del img # 4.3 基本的编解码器 # Python 自带了超过 100 种编解码器(codec, encoder/decoder),用于在文本和字节之间相互转换。 # 示例 4-5 使用3个编解码器编码字符串“El Niño”,得到的字节序列差异很大 for code in ['latin_1', 'utf_8', 'utf_16']: print(codec, 'El Niño'.encode(codec), sep='\t') # 4.4 了解编解码问题 # 虽然有个一般性的 UnicodeError 异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时)或 UnicodeDecodeError(把二进制序列转换成字符串时)。 # 如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError。 # 4.4.1 处理UnicodeEncodeError # 多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。 # 把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。 # 示例 4-6 编码成字节序列:成功和错误处理 city = 'São Paulo' print(city.encode('utf_8')) print(city.encode('utf_16')) print(city.encode('iso8859_1')) # print(city.encode('cp437')) character maps to <undefined> print(city.encode('cp437', errors='ignore')) print(city.encode('cp437', errors='replace')) print(city.encode('cp437', errors='xmlcharrefreplace')) # 4.4.2 处理UnicodeDecodeError # 不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是有效的 UTF-8 或 UTF-16。 # 因此,把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出UnicodeDecodeError。 # 示例 4-7 把字节序列解码成字符串:成功和错误处理 octets = b'Montr\xe9al' print(octets.decode('cp1252')) print(octets.decode('iso8859_7')) print(octets.decode('koi8_r')) # print(octets.decode('utf_8')) invalid continuation byte print(octets.decode('utf_8', errors='replace')) # 4.4.3 使用预期之外的编码加载模块时抛出的SyntaxError # GNU/Linux 和 OS X 系统大都使用 UTF-8,因此打开在 Windows 系统中使用 cp1252 编码的 .py 文件时可能发生这种情况。 # 注意,这个错误在Windows 版 Python 中也可能会发生,因为 Python 3 为所有平台设置的默认编码都是 UTF-8。 示例 4-8 ola.py:“你好,世界!”的葡萄牙语版 # coding: cp1252 print('Olá, Mundo!') # 4.4.4 如何找出字节序列的编码 # 统一字符编码侦测包 Chardet(https://pypi.python.org/pypi/chardet)就是这样工作的,它能识别所支持的 30 种编码。 # Chardet 是一个 Python 库,可以在程序中使用,不过它也提供了命令行工具 chardetect。下面是它对本章书稿文件的检测报告: $ chardetect 04-text-byte.asciidoc 04-text-byte.asciidoc: utf-8 with confidence 0.99 # 4.4.5 BOM:有用的鬼符 # 4.5 处理文本文件 # 处理文本的最佳实践是“Unicode 三明治”(如图 4-2 所示)。 意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。 # 示例 4-9 一个平台上的编码问题 print(open('cafe.txt', 'w', encoding='utf_8').write('café')) print(open('cafe.txt').read()) # 示例 4-10 仔细分析在 Windows 中运行的示例 4-9,找出并修正问题 # 示例 4-11 探索编码默认值 # 示例 4-12 在Windows 7(SP1)巴西版中的 cmd.exe 中输出的默认编码;PowerShell 输出的结果相同 # 4.6 为了正确比较而规范化Unicode字符串 # 因为 Unicode 有组合字符(变音符号和附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。 # 4.6.1 大小写折叠 # 大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold() 方法(Python 3.3 新增)支持。 # 4.6.2 规范化文本匹配实用函数 # 由前文可知,NFC 和 NFD 可以放心使用,而且能合理比较 Unicode 字符串。对大多数应用来说,NFC 是最好的规范化形式。 # 不区分大小写的比较应该使用 str.casefold()。 # 示例 4-13 normeq.py:比较规范化 Unicode 字符串 # 4.6.3 极端“规范化”:去掉变音符号 # 示例 4-14 去掉全部组合记号的函数(在 sanitize.py 模块中) # 示例 4-15 示例 4-14 中 shave_marks 函数的两个使用示例 # 示例 4-16 删除拉丁字母中组合记号的函数 # 示例 4-17 把一些西文印刷字符转换成 ASCII 字符 # 示例 4-18 示例 4-17 中 asciize 函数的使用示例 # 4.7 Unicode文本排序 # Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽如人意。 # 下面对一个生长在巴西的水果的列表进行排序: fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] print(sorted(fruits)) # 示例 4-19 使用locale.strxfrm函数做排序键 import locale locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8') fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] sorted_fruits = sorted(fruits, key=locale.strxfrm) print(sorted_fruits) # 使用Unicode排序算法排序 # 示例 4-20 使用 pyuca.Collator.sort_key 方法 import pyuca coll = pyuca.Collator() fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] sorted_fruits = sorted(fruits, key=coll.sort_key) print(sorted_fruits) # 4.8 Unicode数据库 # unicodedata 模块中有几个函数用于获取字符的元数据。 # 例如,字符在标准中的官方名称是不是组合字符(如结合波形符构成的变音符号等),以及符号对应的人类可读数值(不是码位)。 # 示例 4-21 展示了unicodedata.name() 和 unicodedata.numeric() 函数,以及字符串的 .isdecimal() 和 .isnumeric() 方法的用法。 # 示例 4-21 Unicode 数据库中数值字符的元数据示例 # 4.9 支持字符串和字节序列的双模式API # 标准库中的一些函数能接受字符串或字节序列为参数,然后根据类型展现不同的行为。re 和 os 模块中就有这样的函数。 # 4.9.1 正则表达式中的字符串和字节序列 # 如果使用字节序列构建正则表达式,\d 和 \w 等模式只能匹配 ASCII 字符;相比之下,如果是字符串模式,就能匹配 ASCII 之外的 Unicode 数字或字母。 # 示例 4-22 ramanujan.py:比较简单的字符串正则表达式和字节序列正则表达式的行为 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)) # 4.9.2 os函数中的字符串和字节序列 # 示例 4-23 把字符串和字节序列参数传给listdir函数得到的结果 os.listdir('.') os.listdir(b'.') # 示例 4-24 使用 surrogateescape 错误处理方式 os.listdir('.') os.listdir(b'.') pi_name_bytes = os.listdir(b'.')[1] pi_name_str = pi_name_bytes.decode('ascii', 'surrogateescape') pi_name_str pi_name_str.encode('ascii', 'surrogateescape') # 4.10 本章小结 # 本章首先澄清了人们对一个字符等于一个字节的误解。随着 Unicode 的广泛使用(80% 的网站已经使用 UTF-8),我们必须把文本字符串与它们在文件中的二进制序列表述区分开,而 Python 3 中这个区分是强制的。 # 4.11 延伸阅读