返回介绍

4.5 处理文本文件

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

处理文本的最佳实践是“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 技术交流群。

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

发布评论

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