Python 文本和字节序列(处理文本文件)

本章将讨论下述话题:

字符、码位和字节表述

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

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

避免和处理编码错误

处理文本文件的最佳实践

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

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

规范化、大小写折叠和暴力移除音调符号的实用函数

使用 locale 模块和 PyUCA 库正确地排序 Unicode 文本

Unicode 数据库中的字符元数据

能处理字符串和字节序列的双模式 API

处理文本文件

处理文本的最佳实践是"Unicode 三明治"(如图 4-2 所示)。 意思是,

要尽早把输入(例如读取文件时)的字节序列解码成字符串。这种三明

治中的"肉片"是程序的业务逻辑,在这里只能处理字符串对象。在其他

处理过程中,一定不能编码或解码。对输出来说,则要尽量晚地把字符

串编码成字节序列。多数 Web 框架都是这样做的,使用框架时很少接

触字节序列。例如,在 Django 中,视图应该输出 Unicode 字符串;

Django 会负责把响应编码成字节序列,而且默认使用 UTF-8 编码。

在 Python 3 中能轻松地采纳 Unicode 三明治的建议,因为内置的 open

函数会在读取文件时做必要的解码,以文本模式写入文件时还会做必要

的编码,所以调用 my_file.read() 方法得到的以及传给

my_file.write(text) 方法的都是字符串对象。

可以看出,处理文本文件很简单。但是,如果依赖默认编码,你会遇到

麻烦。

看一下示例 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 环境变量

https://docs.python.org/3/using/cmdline.html#envvar-

PYTHONIOENCODING),sys.stdout/stdin/stderr 的编码使

用设定的值;否则,继承自所在的控制台;如果输入 / 输出重定向

到文件,则由 locale.getpreferredencoding() 定义。

Python 在二进制数据和字符串之间转换时,内部使用

sys.getdefaultencoding() 获得的编码;Python 3 很少如此,但

仍有发生。6 这个设置不能修改。

sys.getfilesystemencoding() 用于编解码文件名(不是文件内

容)。把字符串参数作为文件名传给 open() 函数时就会使用它;

如果传入的文件名参数是字节序列,那就不经改动直接传给 OS

API。"Unicode HOWTO"一文

https://docs.python.org/3/howto/unicode.html)中说:"在 Windows

中,Python 使用 mbcs 这个名称引用当前配置的编码。"MBCS 是

Multi Byte Character Set(多字节字符集)的首字母缩写,在

Windows 中是陈旧的变长编码,如 gb2312 或 Shift_JIS,而不是

UTF-8。[ 关于这个话题,Stack Overflow 中有一个很好的回答",

Difference between MBCS and UTF-8 on

Windows"(http://stackoverflow.com/questions/3298569/differencebetween-

mbcs-and-utf-8-on-windows)。]

在 GNU/Linux 和 OS X 中,这些编码的默认值都是 UTF-8,而

且多年来都是如此,因此 I/O 能处理所有 Unicode 字符。在

Windows 中,不仅同一个系统中使用不同的编码,还有只支持

ASCII 和 127 个额外的字符的代码页(如 'cp850' 或

'cp1252'),而且不同的代码页之间增加的字符也有所不同。因

此,若不多加小心,Windows 用户更容易遇到编码问题。

综上,locale.getpreferredencoding() 返回的编码是最重要的:这

是打开文件的默认编码,也是重定向到文件的

sys.stdout/stdin/stderr 的默认编码。然而,文档也说道(摘录部

分,https://docs.python.org/3/library/locale.html#locale.getpreferredencoding。

复制代码
locale.getpreferredencoding(do_setlocale=True)

根据用户的偏好设置,返回文本数据的编码。用户的偏好设置在不

同系统中的设定方式不同,而且在某些系统中可能无法通过编程方

式设置,因此这个函数返回的只是猜测的编码......

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

如果遵从 Unicode 三明治的建议,而且始终在程序中显式指定编码,那

将避免很多问题。可惜,即使把字节序列正确地转换成字符串,

Unicode 仍有不尽如人意的地方。接下来的两节讨论的话题对 ASCII 世

界来说很简单,但是在 Unicode 领域就变得相当复杂:文本规范化(即

为了比较而把文本转换成统一的表述)和排序。

相关推荐
码上淘金41 分钟前
【Python】Python常用控制结构详解:条件判断、遍历与循环控制
开发语言·python
Brilliant Nemo44 分钟前
四、SpringMVC实战:构建高效表述层框架
开发语言·python
2301_787552871 小时前
console-chat-gpt开源程序是用于 AI Chat API 的 Python CLI
人工智能·python·gpt·开源·自动化
懵逼的小黑子1 小时前
Django 项目的 models 目录中,__init__.py 文件的作用
后端·python·django
Y3174292 小时前
Python Day23 学习
python·学习
Ai尚研修-贾莲2 小时前
Python语言在地球科学交叉领域中的应用——从数据可视化到常见数据分析方法的使用【实例操作】
python·信息可视化·数据分析·地球科学
格林威3 小时前
Baumer工业相机堡盟工业相机的工业视觉中为什么偏爱“黑白相机”
开发语言·c++·人工智能·数码相机·计算机视觉
橙子199110163 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork3 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
qq_508576093 小时前
if __name__ == ‘__main__‘
python