返回介绍

4.4 了解编解码问题

发布于 2024-02-05 21:59:48 字数 8134 浏览 0 评论 0 收藏 0

虽然有个一般性的 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')  ➏
'Montr��al'

❶ 这些字节序列是使用 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文