- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
4.4 了解编解码问题
虽然有个一般性的 UnicodeError 异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncodeError(把字符串转换成二进制序列时)或 UnicodeDecodeError(把二进制序列转换成字符串时)。如果源码的编码与预期不符,加载 Python 模块时还可能抛出 SyntaxError。接下来的几节说明如何处理这些错误。
出现与 Unicode 有关的错误时,首先要明确异常的类型。导致编码问题的是 UnicodeEncodeError、UnicodeDecodeError,还是如 SyntaxError 的其他错误?解决问题之前必须清楚这一点。
4.4.1 处理UnicodeEncodeError
多数非 UTF 编解码器只能处理 Unicode 字符的一小部分子集。把文本转换成字节序列时,如果目标编码中没有定义某个字符,那就会抛出 UnicodeEncodeError 异常,除非把 errors 参数传给编码方法或函数,对错误进行特殊处理。处理错误的方式如示例 4-6 所示。
示例 4-6 编码成字节序列:成功和错误处理
>>> city = 'São Paulo' >>> city.encode('utf_8') ➊ b'S\xc3\xa3o Paulo' >>> city.encode('utf_16') b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00' >>> city.encode('iso8859_1') ➋ b'S\xe3o Paulo' >>> city.encode('cp437') ➌ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/.../lib/python3.4/encodings/cp437.py", line 12, in encode return codecs.charmap_encode(input,errors,encoding_map) UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined> >>> city.encode('cp437', errors='ignore') ➍ b'So Paulo' >>> city.encode('cp437', errors='replace') ➎ b'S?o Paulo' >>> city.encode('cp437', errors='xmlcharrefreplace') ➏ b'São Paulo'
❶ 'utf_?' 编码能处理任何字符串。
❷ 'iso8859_1' 编码也能处理字符串 'São Paulo'。
❸ 'cp437' 无法编码 'ã'(带波形符的“a”)。默认的错误处理方式 'strict' 抛出 UnicodeEncodeError。
❹ error='ignore' 处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥。
❺ 编码时指定 error='replace',把无法编码的字符替换成 '?';数据损坏了,但是用户知道出了问题。
❻ 'xmlcharrefreplace' 把无法编码的字符替换成 XML 实体。
编解码器的错误处理方式是可扩展的。你可以为 errors 参数注册额外的字符串,方法是把一个名称和一个错误处理函数传给 codecs.register_error 函数。参见 codecs.register_error 函数的文档。
4.4.2 处理UnicodeDecodeError
不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是有效的 UTF-8 或 UTF-16。因此,把二进制序列转换成文本时,如果假设是这两个编码中的一个,遇到无法转换的字节序列时会抛出 UnicodeDecodeError。
另一方面,很多陈旧的 8 位编码——如 'cp1252'、'iso8859_1' 和 'koi8_r'——能解码任何字节序列流而不抛出错误,例如随机噪声。因此,如果程序使用错误的 8 位编码,解码过程悄无声息,而得到的是无用输出。
乱码字符称为鬼符(gremlin)或 mojibake(文字化け,“变形文本”的日文)。
示例 4-7 演示了使用错误的编解码器可能出现鬼符或抛出 UnicodeDecodeError。
示例 4-7 把字节序列解码成字符串:成功和错误处理
>>> octets = b'Montr\xe9al' ➊ >>> octets.decode('cp1252') ➋ 'Montréal' >>> octets.decode('iso8859_7') ➌ 'Montrιal' >>> octets.decode('koi8_r') ➍ 'MontrИal' >>> octets.decode('utf_8') ➎ Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte >>> octets.decode('utf_8', errors='replace') ➏ 'Montral'
❶ 这些字节序列是使用 latin1 编码的“Montréal”;'\xe9' 字节对应“é”。
❷ 可以使用 'cp1252'(Windows 1252)解码,因为它是 latin1 的有效超集。
❸ ISO-8859-7 用于编码希腊文,因此无法正确解释 '\xe9' 字节,而且没有抛出错误。
❹ KOI8-R 用于编码俄文;这里,'\xe9' 表示西里尔字母“И”。
❺ 'utf_8' 编解码器检测到 octets 不是有效的 UTF-8 字符串,抛出 UnicodeDecodeError。
❻ 使用 'replace' 错误处理方式,\xe9 替换成了“”(码位是 U+FFFD),这是官方指定的 REPLACEMENT CHARACTER(替换字符),表示未知字符。
4.4.3 使用预期之外的编码加载模块时抛出的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
GNU/Linux 和 OS X 系统大都使用 UTF-8,因此打开在 Windows 系统中使用 cp1252 编码的 .py 文件时可能发生这种情况。注意,这个错误在 Windows 版 Python 中也可能会发生,因为 Python 3 为所有平台设置的默认编码都是 UTF-8。
为了修正这个问题,可以在文件顶部添加一个神奇的 coding 注释,如示例 4-8 所示。
示例 4-8 ola.py:“你好,世界!”的葡萄牙语版
# coding: cp1252 print('Olá, Mundo!')
现在,Python 3 的源码不再限于使用 ASCII,而是默认使用优秀的 UTF-8 编码,因此要修正源码的陈旧编码(如 'cp1252')问题,最好将其转换成 UTF-8,别去麻烦 coding 注释。如果你用的编辑器不支持 UTF-8,那么是时候换一个了。
源码中能不能使用非 ASCII 名称
Python 3 允许在源码中使用非 ASCII 标识符:
>>> ação = 'PBR' # ação = stock >>> ε = 10**-6 # ε = epsilon
有些人不喜欢这么做。支持始终使用 ASCII 标识符的人认为,这样便于所有人阅读和编辑代码。这些人没切中要害:源码应该便于目标群体阅读和编辑,而不是“所有人”。如果代码属于跨国公司,或者是开源的,想让来自世界各地的人作贡献,那么标识符应该使用英语,也就是说只能使用 ASCII 字符。
但是,如果你是巴西的一位老师,那么使用葡萄牙语正确拼写变量和函数名更便于学生阅读代码。而且,这些学生在本地化的键盘中不难打出变音符号和重音元音字母。
现在,Python 能解析 Unicode 名称,而且源码的默认编码是 UTF-8,我觉得没有任何理由使用不带重音符号的葡萄牙语编写标识符。在 Python 2 中确实不能这么做,除非你也想使用 Python 2 运行代码,否则不必如此。如果使用葡萄牙语命名标识符却不带重音符号的话,这样写出的代码对任何人来说都不易阅读。
这是我作为说葡萄牙语的巴西人的观点,不过我相信也适用于其他国家和文化:选择对团队而言易于阅读的人类语言,然后使用正确的字符拼写。
假如有个文本文件,里面保存的是源码或诗句,但是你不知道它的编码。如何查明真正的编码呢?下一节使用一个推荐的库回答这个问题。
4.4.4 如何找出字节序列的编码
如何找出字节序列的编码?简单来说,不能。必须有人告诉你。
有些通信协议和文件格式,如 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 种编码。Chardet 是一个 Python 库,可以在程序中使用,不过它也提供了命令行工具 chardetect。下面是它对本章书稿文件的检测报告:
$ chardetect 04-text-byte.asciidoc 04-text-byte.asciidoc: utf-8 with confidence 0.99
二进制序列编码文本通常不会明确指明自己的编码,但是 UTF 格式可以在文本内容的开头添加一个字节序标记。参见下一节。
4.4.5 BOM:有用的鬼符
在示例 4-5 中,你可能注意到了,UTF-16 编码的序列开头有几个额外的字节,如下所示:
>>> u16 = 'El Niño'.encode('utf_16') >>> u16 b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'
我指的是 b'\xff\xfe'。这是 BOM,即字节序标记(byte-order mark),指明编码时使用 Intel CPU 的小字节序。
在小字节序设备中,各个码位的最低有效字节在前面:字母 'E' 的码位是 U+0045(十进制数 69),在字节偏移的第 2 位和第 3 位编码为 69 和 0。
>>> list(u16) [255, 254, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
在大字节序 CPU 中,编码顺序是相反的;'E' 编码为 0 和 69。
为了避免混淆,UTF-16 编码在要编码的文本前面加上特殊的不可见字符 ZERO WIDTH NO-BREAK 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:
>>> u16le = 'El Niño'.encode('utf_16le') >>> list(u16le) [69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0] >>> u16be = 'El Niño'.encode('utf_16be') >>> list(u16be) [0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]
如果有 BOM,UTF-16 编解码器会将其过滤掉,为你提供没有前导 ZERO WIDTH NO-BREAK SPACE 字符的真正文本。根据标准,如果文件使用 UTF-16 编码,而且没有 BOM,那么应该假定它使用的是 UTF-16BE(大字节序)编码。然而,Intel x86 架构用的是小字节序,因此有很多文件用的是不带 BOM 的小字节序 UTF-16 编码。
与字节序有关的问题只对一个字(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 编码的。
下面换个话题,讨论 Python 3 处理文本文件的方式。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论