Unicode HOWTO — Python 文档

来自菜鸟教程
Python/docs/3.6/howto/unicode
跳转至:导航、​搜索

Unicode HOWTO

发布
1.12

本 HOWTO 讨论了 Python 对 Unicode 的支持,并解释了人们在尝试使用 Unicode 时经常遇到的各种问题。

Unicode 简介

字符代码的历史

1968 年,美国信息交换标准代码(以其首字母缩写词 ASCII 更为人所知)被标准化。 ASCII 定义了各种字符的数字代码,数值从 0 到 127。 例如,小写字母“a”被指定为 97 作为其代码值。

ASCII 是美国开发的标准,因此它只定义了非重音字符。 有一个“e”,但没有“é”或“Í”。 这意味着需要重音字符的语言不能用 ASCII 忠实地表示。 (实际上,缺少的重音对英语也很重要,其中包含诸如“naïve”和“café”之类的词,并且一些出版物具有需要拼写的房屋风格,例如“coöperate”。)

有一段时间,人们只是编写不显示重音的程序。 在 1980 年代中期,由讲法语的人编写的 Apple II BASIC 程序可能有如下几行:

PRINT "MISE A JOUR TERMINEE"
PRINT "PARAMETRES ENREGISTRES"

这些消息应该包含口音(terminée、paramètre、enregistrés),而且对于会读法语的人来说,它们看起来是错误的。

在 1980 年代,几乎所有的个人计算机都是 8 位的,这意味着字节可以保存 0 到 255 之间的值。 ASCII 码最多只能达到 127,因此一些机器将 128 到 255 之间的值分配给重音字符。 然而,不同的机器有不同的代码,这导致了交换文件的问题。 最终出现了 128-255 范围内的各种常用值集。 有些是真正的标准,由国际标准化组织定义,有些是 事实上的 惯例,由一家或另一家公司发明并成功流行起来。

255 个字符并不是很多。 例如,您不能将西欧使用的重音字符和俄语使用的西里尔字母都放入 128-255 范围内,因为此类字符超过 128 个。

您可以使用不同的代码编写文件(所有俄语文件都使用名为 KOI8 的编码系统,所有法语文件使用名为 Latin1 的不同编码系统),但是如果您想编写引用一些俄语文本的法语文档怎么办? 到了 80 年代,人们开始想要解决这个问题,Unicode 标准化工作开始了。

Unicode 开始使用 16 位字符而不是 8 位字符。 16 位意味着您有 2^16 = 65,536 个不同的值可用,从而可以表示许多不同字母表中的许多不同字符; 最初的目标是让 Unicode 包含每种人类语言的字母表。 事实证明,即使是 16 位也不足以满足该目标,现代 Unicode 规范使用范围更广的代码,从 0 到 1,114,111(基数为 16 的 0x10FFFF)。

有一个相关的 ISO 标准 ISO 10646。 Unicode 和 ISO 10646 最初是分开的工作,但规范与 Unicode 的 1.1 修订版合并。

(此对 Unicode 历史的讨论已高度简化。 准确的历史细节对于理解如何有效地使用 Unicode 不是必需的,但如果您感到好奇,请查阅参考资料中列出的 Unicode 联盟站点或 Unicode 的维基百科条目以获取更多信息。)


定义

character 是文本的最小可能组成部分。 'A'、'B'、'C' 等都是不同的字符。 'È' 和 'Í' 也是如此。 字符是抽象的,会因您所谈论的语言或上下文而异。 例如,欧姆 (Ω) 的符号通常与希腊字母表中的大写字母 omega (Ω) 非常相似(它们甚至在某些字体中可能相同),但这是两个具有不同含义的不同字符。

Unicode 标准描述了字符如何由 码位 表示。 代码点是一个整数值,通常以 16 进制表示。 在标准中,代码点使用符号 U+12CA 来表示值为 0x12ca(十进制为 4,810)的字符。 Unicode 标准包含许多列出字符及其相应代码点的表格:

0061    'a'; LATIN SMALL LETTER A
0062    'b'; LATIN SMALL LETTER B
0063    'c'; LATIN SMALL LETTER C
...
007B    '{'; LEFT CURLY BRACKET

严格来说,这些定义意味着说“这是字符 U+12CA”是没有意义的。 U+12CA 是一个码位,代表某个特定的字符; 在这种情况下,它代表字符“ETHIOPIC SYLLABLE WI”。 在非正式环境中,有时会忘记代码点和字符之间的这种区别。

字符在屏幕或纸张上由一组称为 字形 的图形元素表示。 例如,大写字母 A 的字形是两个对角线笔画和一个水平笔画,但具体细节取决于所使用的字体。 大多数 Python 代码不需要担心字形; 找出要显示的正确字形通常是 GUI 工具包或终端的字体渲染器的工作。


编码

总结上一节:Unicode 字符串是一系列代码点,它们是从 0 到 0x10FFFF(十进制 1,114,111)的数字。 该序列需要表示为内存中的一组字节(即 0 到 255 之间的值)。 将 Unicode 字符串转换为字节序列的规则称为 编码

您可能想到的第一种编码是 32 位整数数组。 在此表示中,字符串“Python”将如下所示:

   P           y           t           h           o           n
0x50 00 00 00 79 00 00 00 74 00 00 00 68 00 00 00 6f 00 00 00 6e 00 00 00
   0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

这种表示很简单,但使用它会带来许多问题。

  1. 它不便携; 不同的处理器以不同的方式对字节进行排序。
  2. 非常浪费空间。 在大多数文本中,大多数代码点小于 127,或者小于 255,因此 0x00 字节占用了大量空间。 与 ASCII 表示所需的 6 个字节相比,上述字符串需要 24 个字节。 RAM 使用量的增加并不重要(台式计算机有 GB 的 RAM,而字符串通常不会那么大),但是将我们对磁盘和网络带宽的使用量增加 4 倍是无法忍受的。
  3. 它与现有的 C 函数(如 strlen())不兼容,因此需要使用新的宽字符串函数系列。
  4. 许多 Internet 标准是根据文本数据定义的,无法处理嵌入零字节的内容。

一般人们不使用这种编码,而是选择其他更高效、更方便的编码。 UTF-8 可能是最常支持的编码; 下文将对此进行讨论。

编码不必处理每一个可能的 Unicode 字符,而且大多数编码都不需要。 例如,将 Unicode 字符串转换为 ASCII 编码的规则很简单; 对于每个代码点:

  1. 如果代码点 < 128,则每个字节与代码点的值相同。
  2. 如果代码点为 128 或更大,则 Unicode 字符串不能用这种编码表示。 (在这种情况下,Python 会引发 UnicodeEncodeError 异常。)

Latin-1,也称为 ISO-8859-1,是一种类似的编码。 Unicode 代码点 0-255 与 Latin-1 值相同,因此转换为这种编码只需将代码点转换为字节值; 如果遇到大于 255 的代码点,则无法将字符串编码为 Latin-1。

编码不必像 Latin-1 那样是简单的一对一映射。 考虑 IBM 的 EBCDIC,它用于 IBM 大型机。 字母值不在一个块中:“a”到“i”的值为 129 到 137,但“j”到“r”的值为 145 到 153。 如果您想使用 EBCDIC 作为编码,您可能会使用某种查找表来执行转换,但这主要是内部细节。

UTF-8 是最常用的编码之一。 UTF 代表“Unicode 转换格式”,'8' 表示编码中使用 8 位数字。 (也有 UTF-16 和 UTF-32 编码,但它们的使用频率不如 UTF-8。)UTF-8 使用以下规则:

  1. 如果代码点 < 128,则由相应的字节值表示。
  2. 如果代码点 >= 128,则将其转换为两个、三个或四个字节的序列,其中该序列的每个字节介于 128 和 255 之间。

UTF-8 有几个方便的特性:

  1. 它可以处理任何 Unicode 代码点。
  2. Unicode 字符串被转换为不包含嵌入零字节的字节序列。 这避免了字节顺序问题,并且意味着 UTF-8 字符串可以由 C 函数(如 strcpy())处理并通过无法处理零字节的协议发送。
  3. 一串 ASCII 文本也是有效的 UTF-8 文本。
  4. UTF-8 相当紧凑; 大多数常用字符可以用一两个字节表示。
  5. 如果字节损坏或丢失,则可以确定下一个 UTF-8 编码代码点的开始并重新同步。 随机 8 位数据也不太可能看起来像有效的 UTF-8。


参考

Unicode Consortium 站点 有字符表、词汇表和 Unicode 规范的 PDF 版本。 为一些困难的阅读做好准备。 网站上还提供了 Unicode 起源和发展的年表

为了帮助理解该标准,Jukka Korpela 编写了 介绍性指南 以阅读 Unicode 字符表。

另一篇 好的介绍性文章 由 Joel Spolsky 撰写。 如果这篇介绍没有让你清楚,你应该在继续之前尝试阅读这篇替代文章。

维基百科条目通常很有帮助; 例如,参见“字符编码”和UTF-8的条目。


Python 的 Unicode 支持

现在您已经了解了 Unicode 的基础知识,我们可以看看 Python 的 Unicode 特性。

字符串类型

自 Python 3.0 起,该语言具有包含 Unicode 字符的 str 类型,这意味着使用 "unicode rocks!"'unicode rocks!' 或三引号字符串语法创建的任何字符串都存储为统一码。

Python 源代码的默认编码是 UTF-8,因此您可以简单地在字符串文字中包含一个 Unicode 字符:

try:
    with open('/tmp/input.txt', 'r') as f:
        ...
except OSError:
    # 'File not found' error message.
    print("Fichier non trouvé")

您可以通过将特殊格式的注释作为源代码的第一行或第二行来使用与 UTF-8 不同的编码:

# -*- coding: <encoding name> -*-

旁注:Python 3 还支持在标识符中使用 Unicode 字符:

répertoire = "/tmp/records.log"
with open(répertoire, "w") as f:
    f.write("test\n")

如果您无法在编辑器中输入特定字符或出于某种原因想要仅保留源代码 ASCII,您还可以在字符串文字中使用转义序列。 (根据您的系统,您可能会看到实际的大写字母 - delta 字形而不是 au 转义符。)

>>> "\N{GREEK CAPITAL LETTER DELTA}"  # Using the character name
'\u0394'
>>> "\u0394"                          # Using a 16-bit hex value
'\u0394'
>>> "\U00000394"                      # Using a 32-bit hex value
'\u0394'

此外,可以使用 bytesdecode() 方法创建字符串。 此方法采用 encoding 参数,例如 UTF-8,以及可选的 errors 参数。

errors 参数指定无法根据编码规则转换输入字符串时的响应。 此参数的合法值为 'strict'(引发 UnicodeDecodeError 异常)、'replace'(使用 U+FFFDREPLACEMENT CHARACTER)、'ignore'(只需将字符从 Unicode 结果中删除),或 'backslashreplace'(插入 \xNN 转义序列)。 以下示例显示了差异:

>>> b'\x80abc'.decode("utf-8", "strict")  
Traceback (most recent call last):
    ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0:
  invalid start byte
>>> b'\x80abc'.decode("utf-8", "replace")
'\ufffdabc'
>>> b'\x80abc'.decode("utf-8", "backslashreplace")
'\\x80abc'
>>> b'\x80abc'.decode("utf-8", "ignore")
'abc'

编码被指定为包含编码名称的字符串。 Python 3.2 提供了大约 100 种不同的编码; 有关列表,请参阅 标准编码 中的 Python 库参考。 一些编码有多个名称; 例如,'latin-1''iso_8859_1''8859' 都是相同编码的同义词。

也可以使用 chr() 内置函数创建单字符 Unicode 字符串,该函数接受整数并返回包含相应代码点的长度为 1 的 Unicode 字符串。 反向操作是内置的 ord() 函数,它接受一个字符的 Unicode 字符串并返回代码点值:

>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344

转换为字节

bytes.decode() 的相反方法是 str.encode(),它返回 Unicode 字符串的 bytes 表示,以请求的 编码]编码

errors 参数与 decode() 方法的参数相同,但支持更多可能的处理程序。 除了 'strict''ignore''replace'(在这种情况下插入一个问号而不是不可编码的字符),还有 'xmlcharrefreplace'(插入 XML 字符引用)、backslashreplace(插入 \uNNNN 转义序列)和 namereplace(插入 \N{...} 转义序列)。

以下示例显示了不同的结果:

>>> u = chr(40960) + 'abcd' + chr(1972)
>>> u.encode('utf-8')
b'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')  
Traceback (most recent call last):
    ...
UnicodeEncodeError: 'ascii' codec can't encode character '\ua000' in
  position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
b'abcd'
>>> u.encode('ascii', 'replace')
b'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
b'&#40960;abcd&#1972;'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

用于注册和访问可用编码的低级例程可在 codecs 模块中找到。 实现新编码还需要了解 codecs 模块。 然而,这个模块返回的编码和解码函数通常比较低级而不舒服,编写新的编码是一项专门的任务,因此本 HOWTO 不会涉及该模块。


Python 源代码中的 Unicode 文字

在 Python 源代码中,可以使用 \u 转义序列编写特定的 Unicode 代码点,后跟四个给出代码点的十六进制数字。 \U 转义序列类似,但需要八个十六进制数字,而不是四个:

>>> s = "a\xac\u1234\u20ac\U00008000"
... #     ^^^^ two-digit hex escape
... #         ^^^^^^ four-digit Unicode escape
... #                     ^^^^^^^^^^ eight-digit Unicode escape
>>> [ord(c) for c in s]
[97, 172, 4660, 8364, 32768]

对大于 127 的代码点使用转义序列在小剂量下是没问题的,但是如果您使用许多重音字符,就像在包含法语或其他一些使用重音的语言的消息的程序中一样,这会变得很烦人。 您还可以使用 chr() 内置函数组合字符串,但这更加乏味。

理想情况下,您希望能够以您的语言的自然编码编写文字。 然后,您可以使用您最喜欢的编辑器编辑 Python 源代码,该编辑器会自然地显示重音字符,并在运行时使用正确的字符。

默认情况下,Python 支持以 UTF-8 编写源代码,但如果您声明正在使用的编码,则几乎可以使用任何编码。 这是通过在源文件的第一行或第二行包含一个特殊注释来完成的:

#!/usr/bin/env python
# -*- coding: latin-1 -*-

u = 'abcdé'
print(ord(u[-1]))

该语法的灵感来自 Emacs 用于指定文件局部变量的符号。 Emacs 支持许多不同的变量,但 Python 仅支持“编码”。 -*- 符号向 Emacs 表明该注释是特殊的; 它们对 Python 没有意义,而是一种约定。 Python 在注释中查找 coding: namecoding=name

如果您不包含这样的注释,则使用的默认编码将是前面提到的 UTF-8。 另见 PEP 263 了解更多信息。


Unicode 属性

Unicode 规范包括一个有关代码点的信息数据库。 对于每个定义的代码点,信息包括字符的名称、类别、数值(如果适用)(Unicode 具有表示罗马数字和分数的字符,例如三分之一和五分之四)。 还有一些与代码点在双向文本中的使用相关的属性和其他与显示相关的属性。

下面的程序显示关于几个字符的一些信息,并打印一个特定字符的数值:

import unicodedata

u = chr(233) + chr(0x0bf2) + chr(3972) + chr(6000) + chr(13231)

for i, c in enumerate(u):
    print(i, '%04x' % ord(c), unicodedata.category(c), end=" ")
    print(unicodedata.name(c))

# Get numeric value of second character
print(unicodedata.numeric(u[1]))

运行时,这会打印:

0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 No TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lo TAGBANWA LETTER SA
4 33af So SQUARE RAD OVER S SQUARED
1000.0

类别代码是描述字符性质的缩写。 这些被分组为“字母”、“数字”、“标点符号”或“符号”等类别,而这些类别又分为子类别。 从上面的输出中获取代码,'Ll' 表示“字母,小写”,'No' 表示“数字,其他”,'Mn' 表示“标记,非空格”,[ X148X] 是“符号,其他”。 有关类别代码列表,请参阅 Unicode 字符数据库文档 通用类别值部分。


Unicode 正则表达式

re 模块支持的正则表达式可以以字节或字符串形式提供。 某些特殊字符序列(例如 \d\w)具有不同的含义,具体取决于模式是以字节还是字符串形式提供的。 例如,\d 将匹配字节中的字符 [0-9],但在字符串中将匹配 'Nd' 类别中的任何字符。

此示例中的字符串具有用泰语和阿拉伯数字书写的数字 57:

import re
p = re.compile(r'\d+')

s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))

执行时,\d+ 会匹配泰语数字并打印出来。 如果您向 compile() 提供 re.ASCII 标志,则 \d+ 将匹配子字符串“57”。

类似地,\w 匹配多种 Unicode 字符,但仅 [a-zA-Z0-9_] 以字节为单位,或者如果提供 re.ASCII,并且 \s 将匹配任一 Unicode空白字符或 [ \t\n\r\f\v]


参考

关于 Python 的 Unicode 支持的一些很好的替代讨论是:

str 类型在 Python 库参考 文本序列类型 — str 中进行了描述。

unicodedata 模块的文档。

codecs 模块的文档。

Marc-André Lemburg 在 EuroPython 2002 上做了一个题为“Python 和 Unicode”(PDF 幻灯片) 的演讲 。 这些幻灯片很好地概述了 Python 2 的 Unicode 功能的设计(其中 Unicode 字符串类型称为 unicode,文字以 u 开头)。


读取和写入 Unicode 数据

一旦您编写了一些处理 Unicode 数据的代码,下一个问题就是输入/输出。 你如何将Unicode字符串放入你的程序中,你如何将Unicode转换成适合存储或传输的形式?

根据您的输入源和输出目的地,您可能不需要做任何事情; 您应该检查应用程序中使用的库是否原生支持 Unicode。 例如,XML 解析器经常返回 Unicode 数据。 许多关系数据库还支持 Unicode 值列,并且可以从 SQL 查询返回 Unicode 值。

Unicode 数据在写入磁盘或通过套接字发送之前通常会转换为特定的编码。 可以自己完成所有工作:打开一个文件,从中读取一个 8 位字节对象,然后使用 bytes.decode(encoding) 转换字节。 但是,不推荐手动方法。

一个问题是编码的多字节性质; 一个 Unicode 字符可以用几个字节表示。 如果要以任意大小的块(例如 1024 或 4096 字节)读取文件,则需要编写错误处理代码来捕获在末尾仅读取编码单个 Unicode 字符的部分字节的情况。一块。 一种解决方案是将整个文件读入内存,然后执行解码,但这会阻止您处理非常大的文件; 如果您需要读取 2 GiB 的文件,则需要 2 GiB 的 RAM。 (更多,真的,因为至少有一段时间你需要在内存中同时拥有编码字符串和它的 Unicode 版本。)

解决方案是使用低级解码接口来捕获部分编码序列的情况。 实现这一点的工作已经为您完成:内置的 open() 函数可以返回一个类文件对象,该对象假定文件的内容采用指定的编码,并接受 Unicode 参数用于诸如如 read()write()。 这通过 open()'s encodingerrors 参数起作用,这些参数的解释就像 str.encode()bytes.decode()

因此,从文件中读取 Unicode 很简单:

with open('unicode.txt', encoding='utf-8') as f:
    for line in f:
        print(repr(line))

也可以在更新模式下打开文件,允许读取和写入:

with open('test', encoding='utf-8', mode='w+') as f:
    f.write('\u4500 blah blah blah\n')
    f.seek(0)
    print(repr(f.readline()[:1]))

Unicode 字符 U+FEFF 用作字节顺序标记 (BOM),通常作为文件的第一个字符写入,以帮助自动检测文件的字节顺序。 某些编码(例如 UTF-16)期望在文件开头出现 BOM; 当使用这种编码时,BOM 将自动作为第一个字符写入,并在读取文件时静默删除。 这些编码有多种变体,例如用于 little-endian 和 big-endian 编码的 'utf-16-le' 和 'utf-16-be',它们指定一种特定的字节顺序并且不跳过 BOM。

在某些地区,在 UTF-8 编码文件的开头使用“BOM”也是惯例; 该名称具有误导性,因为 UTF-8 不依赖于字节顺序。 该标记只是声明文件以 UTF-8 编码。 使用“utf-8-sig”编解码器自动跳过标记(如果存在)以读取此类文件。

Unicode 文件名

当今常用的大多数操作系统都支持包含任意 Unicode 字符的文件名。 通常这是通过将 Unicode 字符串转换为某种因系统而异的编码来实现的。 例如,Mac OS X 使用 UTF-8,而 Windows 使用可配置的编码; 在 Windows 上,Python 使用名称“mbcs”来指代当前配置的任何编码。 在 Unix 系统上,只有设置了 LANGLC_CTYPE 环境变量,才会有文件系统编码; 如果没有,默认编码是 UTF-8。

sys.getfilesystemencoding() 函数返回要在当前系统上使用的编码,以防您想手动进行编码,但没有太多理由费心。 打开文件进行读取或写入时,通常只需提供 Unicode 字符串作为文件名,它就会自动转换为正确的编码:

filename = 'filename\u4500abc'
with open(filename, 'w') as f:
    f.write('blah\n')

os 模块中的函数,例如 os.stat() 也将接受 Unicode 文件名。

os.listdir() 函数返回文件名并引发一个问题:它应该返回文件名的 Unicode 版本,还是应该返回包含编码版本的字节? os.listdir() 两者都可以,具体取决于您提供的目录路径是字节还是 Unicode 字符串。 如果您传递一个 Unicode 字符串作为路径,文件名将使用文件系统的编码进行解码并返回一个 Unicode 字符串列表,而传递一个字节路径将返回文件名作为字节。 例如,假设默认文件系统编码为 UTF-8,运行以下程序:

fn = 'filename\u4500abc'
f = open(fn, 'w')
f.close()

import os
print(os.listdir(b'.'))
print(os.listdir('.'))

将产生以下输出:

amk:~$ python t.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]

第一个列表包含 UTF-8 编码的文件名,第二个列表包含 Unicode 版本。

请注意,在大多数情况下,应使用 Unicode API。 字节 API 应仅用于存在不可解码文件名的系统,即 Unix系统。


编写可识别 Unicode 的程序的技巧

本节提供了一些关于编写处理 Unicode 的软件的建议。

最重要的提示是:

软件应该只在内部处理 Unicode 字符串,尽快解码输入数据并只在最后对输出进行编码。


如果您尝试编写同时接受 Unicode 和字节字符串的处理函数,您会发现您的程序在组合这两种不同类型的字符串的任何地方都容易受到错误的影响。 没有自动编码或解码:如果你这样做 str + bytes,一个 TypeError 将被引发。

当使用来自 Web 浏览器或其他不受信任来源的数据时,常用的技术是在生成的命令行中使用字符串或将其存储在数据库中之前检查字符串中的非法字符。 如果您这样做,请小心检查解码的字符串,而不是编码的字节数据; 一些编码可能具有有趣的特性,例如不是双射的或不完全兼容 ASCII。 如果输入数据还指定了编码,则尤其如此,因为攻击者可以选择一种巧妙的方式将恶意文本隐藏在编码的字节流中。

在文件编码之间转换

StreamRecoder 类可以在编码之间透明地转换,采用以编码 #1 返回数据的流,并且表现得像以编码 #2 返回数据的流。

例如,如果您有一个使用 Latin-1 格式的输入文件 f,您可以用 StreamRecoder 包装它以返回以 UTF-8 编码的字节:

new_f = codecs.StreamRecoder(f,
    # en/decoder: used by read() to encode its results and
    # by write() to decode its input.
    codecs.getencoder('utf-8'), codecs.getdecoder('utf-8'),

    # reader/writer: used to read and write to the stream.
    codecs.getreader('latin-1'), codecs.getwriter('latin-1') )

未知编码的文件

如果您需要对文件进行更改,但不知道文件的编码,该怎么办? 如果您知道编码与 ASCII 兼容并且只想检查或修改 ASCII 部分,您可以使用 surrogateescape 错误处理程序打开文件:

with open(fname, 'r', encoding="ascii", errors="surrogateescape") as f:
    data = f.read()

# make changes to the string 'data'

with open(fname + '.new', 'w',
          encoding="ascii", errors="surrogateescape") as f:
    f.write(data)

surrogateescape 错误处理程序会将任何非 ASCII 字节解码为 Unicode 专用区域中的代码点,范围从 U+DC80 到 U+DCFF。 当在编码数据并将其写回时使用 surrogateescape 错误处理程序时,这些私有代码点将被转换回相同的字节。


参考

Mastering Python 3 Input/Output(David Beazley 在 PyCon 2010 上的演讲)的一个部分讨论了文本处理和二进制数据处理。

Marc-André Lemburg 的演讲“Writing Unicode-aware Applications in Python” PDF 幻灯片讨论了字符编码问题以及如何国际化和本地化应用程序。 这些幻灯片仅涵盖 Python 2.x。

The Guts of Unicode in Python 是 Benjamin Peterson 在 PyCon 2013 上的演讲,讨论了 Python 3.3 中的内部 Unicode 表示。


致谢

本文档的初稿由 Andrew Kuchling 撰写。 此后,Alexander Belopolsky、Georg Brandl、Andrew Kuchling 和 Ezio Melotti 对其进行了进一步修订。

感谢以下指出本文错误或提出建议的人:Éric Araujo、Nicholas Bastin、Nick Coghlan、Marius Gedminas、Kent Johnson、Ken Krugler、Marc-André Lemburg、Martin von Löwis、Terry J. 里迪,查德·惠特克。