流畅的python:4,文本和字节序列

it2023-05-26  89

本章将讨论下述话题:

• 字符、码位和字节表述

• bytes、bytearray 和 memoryview 等二进制序列的独特特性

 • 全部 Unicode 和陈旧字符集的编解码器

 • 避免和处理编码错误

• 处理文本文件的最佳实践

 • 默认编码的陷阱和标准 I/O 的问题

 • 规范化 Unicode 文本,进行安全的比较

 

4.1 字符问题

@,从Python 3 的 str 对象中获取 的元素是Unicode 字符,这相当于从Python 2 的 unicode 对象中获取的元素,而不是从 Python 2 的 str 对象中获取的原始字节序列。

4.2 字节概要

@Python 内置 了两种基本的二进制序列类型:Python 3 引入的不可变 bytes 类型和 Python 2.6 添加的可 变 bytearray 类型。( Python 2.6 也引入了 bytes 类型,但那只不过是 str 类型的别名,与 Python 3 的 bytes 类型不同。)

@ 格 式 化 方 法(format 和 format_map)和几个处理Unicode 数 据 的 方 法( 包 括 casefold、isdecimal、isidentifier、isnumeric、isprintable 和 encode)

@str 类型的其他方法都支持 bytes 和 bytearray 类型。这意味着,我们可以使用熟悉的字符串方 法处理二进制序列,如 endswith、replace、strip、translate、upper 等,只有少数几个 其他方法的参数是 bytes 对象,而不是 str 对象.

@

二进制序列有个类方法是 str 没有的,名为 fromhex,它的作用是解析十六进制数字对 (数字对之间的空格是可选的),构建二进制序列:

>>> bytes.fromhex('31 4B CE A9')

b'1K\xce\xa9'

@构建 bytes 或 bytearray 实例还可以调用各自的构造方法,传入下述参数。

• 一个 str 对象和一个 encoding 关键字参数。  

• 一个可迭代对象,提供 0~255 之间的数值

• 一个实现了缓冲协议的对象(如 bytes、bytearray、memoryview、array.array);此时, 把源对象中的字节序列复制到新建的二进制序列中

@使用缓冲类对象创建 bytes 或 bytearray 对象时,始终复制源对象中的字节序列。与之相反, memoryview 对象允许在二进制数据结构之间共享内存。如果想从二进制序列中提取结构化信 息,struct 模块是重要的工具

结构体和内存视图

@struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有 一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理 bytes、 bytearray 和 memoryview 对象

 

4.3 基本的编解码器

@每个编解码器都有一个名称,如 'utf_8',而且经常有几个别名,如 'utf8'、'utf8' 和 'U8'。这些名称可以传给 open()、str.encode()、bytes.decode() 等函数的 encoding 参数

@utf8:目前 Web 中最常见的 8 位编码; 3 与 ASCII 兼容(纯 ASCII 文本是有效的 UTF-8 文本)。

4.4 了解编解码问题

@出现与Unicode 有关的错误时,首先要明确异常的类型。导致编码问题的是UnicodeEncodeError、UnicodeDecodeError,还是如 SyntaxError 的其他错误?解决问题之前必须清楚这一点。

4.4.1 处理UnicodeEncodeError

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

@ city.encode('cp437', errors='ignore')

 编解码器的错误处理方式是可扩展的。你可以为 errors 参数注册额外的字符 串,方法是把一个名称和一个错误处理函数传给 codecs.register_error 函 数。

4.4.2 处理UnicodeDecodeError

@ octets.decode('utf_8', errors='replace')

 

4.4.3 使用预期之外的编码加载模块时抛出的SyntaxError

@Python 3 默认使用 UTF-8 编码源码,Python 2(从 2.5 开始)则默认使用 ASCII。如果加载 的 .py 模块中包含 UTF-8 之外的数据,而且没有声明编码,会得到的SyntaxError  错误

@为了修正这个问题,可以在文件顶部添加一个神奇的 coding 注释

# coding: cp1252

 

4.4.4 如何找出字节序列的编码

@如何找出字节序列的编码?简单来说,不能。必须有人告诉你。

@统一字符编码侦测包 Chardet(https://pypi.python.org/pypi/chardet)就是这样工作的,它能 识别所支持的 30 种编码。Chardet 是一个 Python 库,可以在程序中使用,不过它也提供了 命令行工具 chardetect。

4.4.5 BOM:有用的鬼符

@我指的是 b'\xff\xfe'。这是BOM,即字节序标记(byte-order mark),指明编码时使用 Intel CPU 的小字节序。

@UTF-16 有两个变种:UTF-16LE,显式指明使用小字节序; UTF-16BE,显式指明使用大字 节序。如果使用这两个变种,不会生成 BOM

@。根据标准,如果文件使用 UTF-16 编码,而且没有 BOM,那么应 该假定它使用的是 UTF-16BE(大字节序)编码。

@。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 编码的。

4.5 处理文本文件

@处理文本的最佳实践是“Unicode 三明治”(如图 4-2 所示)。 意思是,要尽早把输入(例 如读取文件时)的字节序列解码成字符串。这种三明治中的“肉片”是程序的业务逻辑

@需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开 文件时始终应该明确传入 encoding= 参数,因为不同的设备使用的默认编码 可能不同,有时隔一天也会发生变化。

@除非想判断编码,否则不要在二进制模式中打开文本文件;即便如此,也应 该使用 Chardet,而不是重新发明轮子。

 

编码默认值:一团糟

@locale.getpreferredencoding() 将stdout 重定向到一个文件,这个函数会返回该文件的编码方式

@如果打开文件时没有指定 encoding 参数,默认值由 locale.getpreferredencoding() 提供

@如果设定了 PYTHONIOENCODING 环境变量, sys.stdout/stdin/stderr 的编码使用设定的值;否则,继承自 所在的控制台;如果输入 / 输出重定向到文件,则由 locale.getpreferredencoding() 定义。

@Python 在二进制数据和字符串之间转换时,内部使用 sys.getdefaultencoding() 获得的 编码;Python3 很少如此,但仍有发生。 这个设置不能修改。

@sys.getfilesystemencoding() 用于编解码文件名(不是文件内容)。把字符串参数作为文件 名传给 open() 函数时就会使用它;如果传入的文件名参数是字节序列,那就不经改动直接 传给OS API

@,locale.getpreferredencoding() 返回的编码是最重要的:这是打开文件的默认编 码,也是重定向到文件的 sys.stdout/stdin/stderr 的默认编码。

@因此,关于编码默认值的最佳建议是:别依赖默认值。

4.6 为了正确比较而规范化Unicode字符串

@在 Unicode 标准中,'é' 和 'e\u0301' 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作 相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。

@这个问题的解决方案是使用 unicodedata.normalize 函数提供的 Unicode 规范化。这个函数 的第一个参数是这 4 个字符串中的一个:'NFC'、'NFD'、'NFKC' 和 'NFKD'。

@:NFC(Normalization Form C)使用最少的码位构成等价的字符串,而 NFD 把组合字符分 解成基字符和单独的组合字符。这两种规范化方式都能让比较行为符合预期

@西方键盘通常能输出组合字符,因此用户输入的文本默认是NFC 形式。不过,安全起 见,保存文本之前,最好使用 normalize('NFC', user_text) 清洗字符串。NFC 也是 W3C 的规范推荐的规范化形式

@使用 NFC 时,有些单字符会被规范成另一个单字符。例如,电阻的单位欧姆(Ω)会被 规范成希腊字母大写的欧米加。这两个字符在视觉上是一样的,但是比较时并不相等,因 此要规范化,防止出现意外

@NFKC 或 NFKD 可能会损失或曲解信息,但是可以为搜索 和索引提供便利的中间表述:用户搜索 '1 ⁄ 2 inch' 时,如果还能找到包含 '½ inch' 的文 档,那么用户会感到满意

@使用 NFKC 和 NFKD 规范化形式时要小心,而且只能在特殊情况中使用,例 如搜索和索引,而不能用于持久存储,因为这两种转换会导致数据损失。

4.6.1 大小写折叠

@大小写折叠其实就是把所有文本变成小写,再做些其他转换。这个功能由 str.casefold() 方法(Python 3.3 新增)支持。

@对于只包含 latin1 字符的字符串 s,s.casefold() 得到的结果与 s.lower() 一样,唯有两 个例外:微符号 'µ' 会变成小写的希腊字母“μ”(在多数字体中二者看起来一样);德语  Eszett(“ sharp s”, ß)会变成“ss”。

4.6.2 规范化文本匹配实用函数

@对大多数应 用来说,NFC 是最好的规范化形式。不区分大小写的比较应该使用 str.casefold()。

@如果要处理多语言文本,工具箱中应该有示例 4-13 中的 nfc_equal 和 fold_equal 函数

 

4.6.3 极端“规范化”:去掉变音符号

@除了模糊搜索,去掉变音符号还能让 URL 更易于阅读,至少对拉丁语系语言是如此。

@ str.maketrans方法用于创建字符映射的转换表

@translate() 方法根据参数table给出的表转换字符串中的字符

4.7 Unicode文本排序

@在 Python 中,非 ASCII 文本的标准排序方式是使用 locale.strxfrm 函数,根据 locale 模 块的文档,这 个函数会“把字符串转换成适合所在区域进行比较的形式”。

@使用 locale.strxfrm 函数做排序键之前,要调用 setlocale(LC_COLLATE, «your_locale»)。还要祈祷操作系统支 持这项设置。

@因此,标准库提供的国际化排序方案可用,但是似乎只支持GNU/Linux(可能也支持 Windows,但你得是专家)。即便如此,还要依赖区域设置,而这会为部署带来问题。

幸好,有个较为简单的方案:PyPI 中的 PyUCA 库。

使用Unicode排序算法排序

@PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给 Collator() 构 造 方 法。PyUCA 默认使用项目自带的allkeys.txt(https://github.com/ jtauber/pyuca),这就是 Unicode 6.3.0 的副本。

4.8 Unicode数据库

@Unicode 标准提供了一个完整的数据库(许多格式化的文本文件),不仅包括码位与字符 名称之间的映射,还有各个字符的元数据,以及字符之间的关系。例如,Unicode 数据库 记录了字符是否可以打印、是不是字母、是不是数字,或者是不是其他数值符号。

@。re 模块对 Unicode 的支持并不充分。PyPI 中有个新开发的 regex 模块,它的最终目的是取代 re 模块,以提供更好的 Unicode 支持

@一个新的趋势——双模式 API, 即提供的函数能接受字符串或字节序列为参数,然后根据类型进行特殊处理。

4.9 支持字符串和字节序列的双模式API

@标准库中的一些函数能接受字符串或字节序列为参数,然后根据类型展现不同的行为。re 和 os 模块中就有这样的函数。

4.9.1 正则表达式中的字符串和字节序列

@:可以使用正则表达式搜索字符串和字节 序列,但是在后一种情况中,ASCII 范围外的字节不会当成数字和组成单词的字母。

4.9.2 os函数中的字符串和字节序列

@GNU/Linux 内核不理解 Unicode,因此你可能发现了,对任何合理的编码方案来说,在文 件名中使用字节序列都是无效的,无法解码成字符串。在不同操作系统中使用各种客户端 的文件服务器,在遇到这个问题时尤其容易出错。

@为了便于手动处理字符串或字节序列形式的文件名或路径名,os 模块提供了特殊的编码和 解码函数。

fsencode(filename)

如果 filename 是 str 类型(此外还可能是 bytes 类型),使用 sys.getfilesystemencoding() 返回的编解码器把 filename 编码成字节序列;否则,返回未经修改的 filename 字节 序列。

fsdecode(filename)

如果filename 是 bytes 类型(此外还可能是str 类型),使用sys.getfilesystemen- coding() 返回的编解码器把 filename 解码成字符串;否则,返回未经修改的 filename 字符串。

在 Unix 衍生平台中,这些函数使用 surrogateescape 错误处理方式以 避免遇到意外字节序列时卡住。Windows 使用的错误处理方式是 strict。

4.10 本章小结

@。随着 Unicode 的广泛使用,我们必须把文本字符串与它们在文件中的二进制序列表述区分开, 而 Python 3 中这个区分是强制的。

@打开文本 文件时,encoding= 关键字参数不是必需的,但是应该指定。如果没有指定编码,那么 程序会想方设法生成“纯文本”,如此一来,不一致的默认编码就会导致跨平台不兼容 性。

@我们说明了 Python 用作默认值的几个编码设置,以及如何检测它们:locale. getpreferredencoding()、sys.getfilesystemencoding()、sys.getdefaultencoding(),以 及标准I/O 文件(如 sys.stdout.encoding)的编码

最新回复(0)