- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.5 处理文本文件
处理文本的最佳实践是“Unicode 三明治”(如图 4-2 所示)。4 意思是,要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明治中的“肉片”是程序的业务逻辑,在这里只能处理字符串对象。在其他处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串;Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。
4我第一次见到“Unicode 三明治”这种说法是在 Ned Batchelder 在 US PyCon 2012 上所做的精彩演讲中:“Pragmatic Unicode”。
图 4-2:Unicode 三明治——目前处理文本的最佳实践
在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open 函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要的编码,所以调用 my_file.read() 方法得到的以及传给 my_file.write(text) 方法的都是字符串对象。5
5Python 2.6 或 Python 2.7 用户要使用 io.open() 函数才能得到读写文件时自动执行的解码和编码操作。
可以看出,处理文本文件很简单。但是,如果依赖默认编码,你会遇到麻烦。
看一下示例 4-9 中的控制台会话。你能发现问题吗?
示例 4-9 一个平台上的编码问题(如果在你的机器上运行,它可能会发生,也可能不会)
>>> open('cafe.txt', 'w', encoding='utf_8').write('café') 4 >>> open('cafe.txt').read() 'café'
问题是:写入文件时指定了 UTF-8 编码,但是读取文件时没有这么做,因此 Python 假定要使用系统默认的编码(Windows 1252),于是文件的最后一个字节解码成了字符 'é',而不是 'é'。
我是在 Windows 7 中运行示例 4-9 的。在新版 GNU/Linux 或 Mac OS X 中运行同样的语句不会出问题,因为这几个操作系统的默认编码是 UTF-8,让人误以为一切正常。如果打开文件是为了写入,但是没有指定编码参数,会使用区域设置中的默认编码,而且使用那个编码也能正确读取文件。但是,如果脚本要生成文件,而字节的内容取决于平台或同一平台中的区域设置,那么就可能导致兼容问题。
需要在多台设备中或多种场合下运行的代码,一定不能依赖默认编码。打开文件时始终应该明确传入 encoding= 参数,因为不同的设备使用的默认编码可能不同,有时隔一天也会发生变化。
示例 4-9 中有个奇怪的细节:第一个语句中的 write 函数报告写入了 4 个字符,但是下一行读取时却得到了 5 个字符。示例 4-10 是对示例 4-9 的扩展,对这个问题以及其他细节做了说明。
示例 4-10 仔细分析在 Windows 中运行的示例 4-9,找出并修正问题
>>> fp = open('cafe.txt', 'w', encoding='utf_8') >>> fp ➊ <_io.TextIOWrapper name='cafe.txt' mode='w' encoding='utf_8'> >>> fp.write('café') 4 ➋ >>> fp.close() >>> import os >>> os.stat('cafe.txt').st_size 5 ➌ >>> fp2 = open('cafe.txt') >>> fp2 ➍ <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='cp1252'> >>> fp2.encoding ➎ 'cp1252' >>> fp2.read() 'café' ➏ >>> fp3 = open('cafe.txt', encoding='utf_8') ➐ >>> fp3 <_io.TextIOWrapper name='cafe.txt' mode='r' encoding='utf_8'> >>> fp3.read() 'café' ➑ >>> fp4 = open('cafe.txt', 'rb') ➒ >>> fp4 <_io.BufferedReader name='cafe.txt'> ➓ >>> fp4.read() ⓫ b'caf\xc3\xa9'
❶ 默认情况下,open 函数采用文本模式,返回一个 TextIOWrapper 对象。
❷ 在 TextIOWrapper 对象上调用 write 方法返回写入的 Unicode 字符数。
❸ os.stat 报告文件中有 5 个字节;UTF-8 编码的 'é' 占两个字节,0xc3 和 0xa9。
❹ 打开文本文件时没有显式指定编码,返回一个 TextIOWrapper 对象,编码是区域设置中的默认值。
❺ TextIOWrapper 对象有个 encoding 属性;查看它,发现这里的编码是 cp1252。
❻ 在 Windows cp1252 编码中,0xc3 字节是“Ô(带波形符的 A),0xa9 字节是版权符号。
❼ 使用正确的编码打开那个文件。
❽ 结果符合预期:得到的是四个 Unicode 字符 'café'。
❾ 'rb' 标志指明在二进制模式中读取文件。
❿ 返回的是 BufferedReader 对象,而不是 TextIOWrapper 对象。
⓫读取返回的字节序列,结果与预期相符。
除非想判断编码,否则不要在二进制模式中打开文本文件;即便如此,也应该使用 Chardet,而不是重新发明轮子(参见 4.4.4 节)。常规代码只应该使用二进制模式打开二进制文件,如光栅图像。
示例 4-10 的问题是,打开文本文件时依赖默认设置。默认设置有许多来源,参见下一节。
编码默认值:一团糟
有几个设置对 Python I/O 的编码默认值有影响,如示例 4-11 中的 default_encodings.py 脚本所示。
示例 4-11 探索编码默认值
import sys, locale expressions = """ locale.getpreferredencoding() type(my_file) my_file.encoding sys.stdout.isatty() sys.stdout.encoding sys.stdin.isatty() sys.stdin.encoding sys.stderr.isatty() sys.stderr.encoding sys.getdefaultencoding() sys.getfilesystemencoding() """ my_file = open('dummy', 'w') for expression in expressions.split(): value = eval(expression) print(expression.rjust(30), '->', repr(value))
示例 4-11 在 GNU/Linux(Ubuntu 14.04)和 OS X(Mavericks 10.9)中的输出一样,表明这些系统中始终使用 UTF-8:
$ python3 default_encodings.py locale.getpreferredencoding() -> 'UTF-8' type(my_file) -> <class '_io.TextIOWrapper'> my_file.encoding -> 'UTF-8' sys.stdout.isatty() -> True sys.stdout.encoding -> 'UTF-8' sys.stdin.isatty() -> True sys.stdin.encoding -> 'UTF-8' sys.stderr.isatty() -> True sys.stderr.encoding -> 'UTF-8' sys.getdefaultencoding() -> 'utf-8' sys.getfilesystemencoding() -> 'utf-8'
然而,在 Windows 中的输出有所不同,如示例 4-12 所示。
示例 4-12 在Windows 7(SP1)巴西版中的 cmd.exe 中输出的默认编码;PowerShell 输出的结果相同
Z:\>chcp ➊ Página de código ativa: 850 Z:\>python default_encodings.py ➋ locale.getpreferredencoding() -> 'cp1252' ➌ type(my_file) -> <class '_io.TextIOWrapper'> my_file.encoding -> 'cp1252' ➍ sys.stdout.isatty() -> True ➎ sys.stdout.encoding -> 'cp850' ➏ sys.stdin.isatty() -> True sys.stdin.encoding -> 'cp850' sys.stderr.isatty() -> True sys.stderr.encoding -> 'cp850' sys.getdefaultencoding() -> 'utf-8' sys.getfilesystemencoding() -> 'mbcs'
➊ chcp 输出当前控制台激活的代码页:850。
➋ 运行 default_encodings.py,把结果输出到控制台。
➌ locale.getpreferredencoding() 是最重要的设置。
➍ 文本文件默认使用 locale.getpreferredencoding()。
➎ 输出到控制台中,因此 sys.stdout.isatty() 返回 True。
➏ 因此,sys.stdout.encoding 与控制台的编码相同。
如果把输出重定向到文件,如下所示:
Z:\>python default_encodings.py > encodings.log
sys.stdout.isatty() 的返回值会变成 False,sys.stdout.encoding 会设为 locale.getpreferredencoding(),在那台设备中是 'cp1252'。
注意,示例 4-12 中有 4 种不同的编码。
如果打开文件时没有指定 encoding 参数,默认值由 locale.getpreferredencoding() 提供(在示例 4-12 中是 'cp1252')。
如果设定了 PYTHONIOENCODING 环境变量,sys.stdout/stdin/stderr 的编码使用设定的值;否则,继承自所在的控制台;如果输入 / 输出重定向到文件,则由 locale.getpreferredencoding() 定义。
Python 在二进制数据和字符串之间转换时,内部使用 sys.getdefaultencoding() 获得的编码;Python 3 很少如此,但仍有发生。6 这个设置不能修改。7
sys.getfilesystemencoding() 用于编解码文件名(不是文件内容)。把字符串参数作为文件名传给 open() 函数时就会使用它;如果传入的文件名参数是字节序列,那就不经改动直接传给 OS API。“Unicode HOWTO”一文中说:“在 Windows 中,Python 使用 mbcs 这个名称引用当前配置的编码。”MBCS 是 Multi Byte Character Set(多字节字符集)的首字母缩写,在 Windows 中是陈旧的变长编码,如 gb2312 或 Shift_JIS,而不是 UTF-8。 [关于这个话题,Stack Overflow 中有一个很好的回答,“Difference between MBCS and UTF-8 on Windows”。]
6研究这个话题时,我在 Python 内部找不到把字节序列转换成字符串的情况。Python 核心开发者 Antoine Pitrou 在 comp.python.devel 邮件列表中说,CPython 的内部函数“在 py3k 中很少这么做”。
7Python 2 对 sys.setdefaultencoding 函数的使用方式不当,Python 3 的文档中已经没有这个函数。这个函数是供核心开发者使用的,用于在内部的默认编码未定时设置编码。在 comp.python.devel 邮件列表的那个话题中,Marc-André Lemburg 说,用户代码一定不能调用 sys.setdefaultencoding 函数,而且对 CPython 来说,它的值在 Python 2 中只能是 'ascii',在 Python 3 中只能是 'utf-8'。
在 GNU/Linux 和 OS X 中,这些编码的默认值都是 UTF-8,而且多年来都是如此,因此 I/O 能处理所有 Unicode 字符。在 Windows 中,不仅同一个系统中使用不同的编码,还有只支持 ASCII 和 127 个额外的字符的代码页(如 'cp850' 或 'cp1252'),而且不同的代码页之间增加的字符也有所不同。因此,若不多加小心,Windows 用户更容易遇到编码问题。
综上,locale.getpreferredencoding() 返回的编码是最重要的:这是打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr 的默认编码。然而,文档也说道(摘录部分):
locale.getpreferredencoding(do_setlocale=True)
根据用户的偏好设置,返回文本数据的编码。用户的偏好设置在不同系统中的设定方式不同,而且在某些系统中可能无法通过编程方式设置,因此这个函数返回的只是猜测的编码……
因此,关于编码默认值的最佳建议是:别依赖默认值。
如果遵从 Unicode 三明治的建议,而且始终在程序中显式指定编码,那将避免很多问题。可惜,即使把字节序列正确地转换成字符串,Unicode 仍有不尽如人意的地方。接下来的两节讨论的话题对 ASCII 世界来说很简单,但是在 Unicode 领域就变得相当复杂:文本规范化(即为了比较而把文本转换成统一的表述)和排序。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论