Unicode HOWTO

版本:

1.12

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

Unicode 简介

定义

今天的程序需要能够处理各种各样的字符。应用程序通常会进行国际化,以各种用户可选择的语言显示消息和输出;同一个程序可能需要用英语、法语、日语、希伯来语或俄语输出错误消息。Web 内容可以用上述任何一种语言编写,并且还可以包含各种表情符号。Python 的字符串类型使用 Unicode 标准来表示字符,这使得 Python 程序可以使用所有这些不同的可能字符。

Unicode (https://www.unicode.org/) 是一种旨在列出人类语言使用的每个字符并为每个字符提供其唯一的代码的规范。Unicode 规范不断修订和更新,以添加新的语言和符号。

一个 字符 是文本的最小组成部分。“A”、“B”、“C”等等都是不同的字符。“È”和“Í”也是。字符会因你所谈论的语言或上下文而异。例如,有一个表示“罗马数字一”的字符 ‘Ⅰ’,它与大写字母 ‘I’ 是分开的。它们通常看起来相同,但这是两个具有不同含义的不同字符。

Unicode 标准描述了字符如何由 代码点 表示。代码点值是一个介于 0 到 0x10FFFF(大约 110 万个值,实际分配的数量 小于该值)之间的整数。在标准和本文档中,代码点使用 U+265E 的表示法来表示值为 0x265e (十进制 9,822) 的字符。

Unicode 标准包含许多列出字符及其对应代码点的表格

0061    'a'; LATIN SMALL LETTER A
0062    'b'; LATIN SMALL LETTER B
0063    'c'; LATIN SMALL LETTER C
...
007B    '{'; LEFT CURLY BRACKET
...
2167    'Ⅷ'; ROMAN NUMERAL EIGHT
2168    'Ⅸ'; ROMAN NUMERAL NINE
...
265E    '♞'; BLACK CHESS KNIGHT
265F    '♟'; BLACK CHESS PAWN
...
1F600   '😀'; GRINNING FACE
1F609   '😉'; WINKING FACE
...

严格来说,这些定义意味着说“这是字符 U+265E”是没有意义的。U+265E 是一个代码点,它表示一些特定的字符;在这种情况下,它表示字符“黑棋骑士”,‘♞’。在非正式的上下文中,有时会忘记代码点和字符之间的这种区别。

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

编码

总结上一节:Unicode 字符串是代码点的序列,代码点是 0 到 0x10FFFF (十进制 1,114,111) 的数字。这个代码点序列需要在内存中表示为一组 代码单元,然后将 代码单元 映射到 8 位字节。将 Unicode 字符串转换为字节序列的规则称为 字符编码,或者简称 编码

您可能想到的第一个编码是使用 32 位整数作为代码单元,然后使用 CPU 的 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 字节占用。上面的字符串需要 24 个字节,而 ASCII 表示只需要 6 个字节。增加的 RAM 使用量并不是太重要(台式计算机有千兆字节的 RAM,并且字符串通常不是那么大),但是将我们对磁盘和网络带宽的使用量扩大 4 倍是无法容忍的。

  3. 它与现有的 C 函数(例如 strlen())不兼容,因此需要使用新的宽字符串函数系列。

因此,这种编码的使用并不多,人们而是选择其他更高效和方便的编码,例如 UTF-8。

UTF-8 是最常用的编码之一,Python 通常默认使用它。UTF 代表“Unicode 转换格式”,而 ‘8’ 表示编码中使用 8 位值。(还有 UTF-16 和 UTF-32 编码,但它们的使用频率低于 UTF-8。)UTF-8 使用以下规则

  1. 如果代码点 < 128,则用相应的字节值表示。

  2. 如果代码点 >= 128,则将其转换为两、三或四个字节的序列,其中序列的每个字节都在 128 和 255 之间。

UTF-8 有几个方便的属性

  1. 它可以处理任何 Unicode 代码点。

  2. Unicode 字符串转换为字节序列,该序列仅在表示空字符 (U+0000) 的位置包含嵌入的零字节。这意味着 UTF-8 字符串可以由 C 函数(例如 strcpy())处理,并通过无法处理零字节作为字符串结尾标记的协议发送。

  3. ASCII 文本字符串也是有效的 UTF-8 文本。

  4. UTF-8 相当紧凑;大多数常用的字符可以用一个或两个字节表示。

  5. 如果字节损坏或丢失,则可以确定下一个 UTF-8 编码的代码点的开头并重新同步。随机的 8 位数据也不太可能看起来像有效的 UTF-8。

  6. UTF-8 是面向字节的编码。该编码指定每个字符由一个或多个字节的特定序列表示。这避免了整数和面向字的编码(如 UTF-16 和 UTF-32)中可能发生的字节顺序问题,在这些编码中,字节序列会因字符串编码所在的硬件而异。

参考资料

Unicode 联盟网站 包含字符图表、词汇表和 Unicode 规范的 PDF 版本。请做好阅读一些困难内容的准备。Unicode 的起源和发展时间表 也可在该网站上找到。

在 Computerphile Youtube 频道上,Tom Scott 简要讨论了 Unicode 和 UTF-8 的历史(9 分 36 秒)。

为了帮助理解该标准,Jukka Korpela 撰写了 关于阅读 Unicode 字符表的入门指南

另一篇 不错的入门文章 是由 Joel Spolsky 撰写的。如果本介绍没有让您理解清楚,您应该在继续之前尝试阅读这篇替代文章。

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

Python 的 Unicode 支持

现在您已经了解了 Unicode 的基本原理,我们可以看看 Python 的 Unicode 功能。

字符串类型

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

Python 源代码的默认编码是 UTF-8,因此您只需在字符串字面量中包含 Unicode 字符即可

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

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

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

如果您无法在编辑器中输入特定字符,或者出于某些原因希望使源代码仅使用 ASCII,您还可以在字符串字面量中使用转义序列。(根据您的系统,您可能会看到实际的大写 delta 字形而不是 u 转义。)

>>> "\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'

此外,可以使用 decode() 方法的 bytes 创建字符串。此方法接受一个 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 附带大约 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 仅支持“coding”。-*- 符号向 Emacs 指示该注释是特殊的;它们对 Python 没有意义,而是一种约定。Python 在注释中查找 coding: namecoding=name

如果您不包含此类注释,则默认使用的编码将为 UTF-8,如前所述。另请参阅 PEP 263 获取更多信息。

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' 表示“标记,非间距”,而 'So' 表示“符号,其他”。有关类别代码的列表,请参阅 Unicode 字符数据库文档的“常规类别值”部分

比较字符串

Unicode 为比较字符串增加了一些复杂性,因为同一组字符可以用不同的代码点序列表示。例如,像 “ê” 这样的字母可以表示为单个代码点 U+00EA,或表示为 U+0065 U+0302,它是 “e” 的代码点,后跟一个 “COMBINING CIRCUMFLEX ACCENT” 的代码点。打印时,它们会产生相同的输出,但一个是长度为 1 的字符串,另一个是长度为 2 的字符串。

用于不区分大小写比较的一种工具是 casefold() 字符串方法,该方法根据 Unicode 标准描述的算法将字符串转换为不区分大小写的形式。此算法对诸如德语字母 “ß”(代码点 U+00DF)之类的字符进行特殊处理,该字符会变成一对小写字母 “ss”。

>>> street = 'Gürzenichstraße'
>>> street.casefold()
'gürzenichstrasse'

第二个工具是 unicodedata 模块的 normalize() 函数,该函数将字符串转换为几种规范化形式之一,其中后跟组合字符的字母将替换为单个字符。normalize() 可用于执行字符串比较,如果两个字符串以不同的方式使用组合字符,则不会错误地报告不相等

import unicodedata

def compare_strs(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)

    return NFD(s1) == NFD(s2)

single_char = 'ê'
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print('length of first string=', len(single_char))
print('length of second string=', len(multiple_chars))
print(compare_strs(single_char, multiple_chars))

运行时,它会输出

$ python compare-strs.py
length of first string= 1
length of second string= 2
True

normalize() 函数的第一个参数是一个字符串,给出所需的规范化形式,可以是“NFC”、“NFKC”、“NFD”和“NFKD”之一。

Unicode 标准还规定了如何进行不区分大小写的比较

import unicodedata

def compare_caseless(s1, s2):
    def NFD(s):
        return unicodedata.normalize('NFD', s)

    return NFD(NFD(s1).casefold()) == NFD(NFD(s2).casefold())

# Example usage
single_char = 'ê'
multiple_chars = '\N{LATIN CAPITAL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'

print(compare_caseless(single_char, multiple_chars))

这将打印 True。(为什么调用 NFD() 两次?因为有一些字符会使 casefold() 返回一个未规范化的字符串,因此需要再次规范化结果。有关讨论和示例,请参见 Unicode 标准的 3.13 节。)

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 字符,但在字节中或如果提供 re.ASCII 时,仅匹配 [a-zA-Z0-9_],而 \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() 函数可以返回一个类似文件的对象,该对象假定文件的内容是指定的编码,并接受诸如 read()write() 等方法的 Unicode 参数。这通过 open()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 将自动作为第一个字符写入,并且在读取文件时将被静默删除。这些编码的变体(例如 'utf-16-le' 和 'utf-16-be',分别用于小端和大端编码)指定一种特定的字节顺序,并且不跳过 BOM。

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

Unicode 文件名

当今常用的操作系统大多数都支持包含任意 Unicode 字符的文件名。通常,这是通过将 Unicode 字符串转换为一些随系统而异的编码来实现的。目前,Python 正在趋向于使用 UTF-8:MacOS 上的 Python 已经使用 UTF-8 几个版本了,Python 3.6 也切换到在 Windows 上使用 UTF-8。在 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('.'))

将产生以下输出

$ python listdir-test.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]

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

请注意,在大多数情况下,您应该坚持使用这些 API 的 Unicode。字节 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 字节解码为 U+DC80 到 U+DCFF 特殊范围内的代码点。当使用 surrogateescape 错误处理程序对数据进行编码并将其写回时,这些代码点将转换回相同的字节。

参考资料

精通 Python 3 输入/输出(David Beazley 在 PyCon 2010 上的演讲)的其中一部分讨论了文本处理和二进制数据处理。

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

Python 中 Unicode 的内部结构是 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. Reedy、Serhiy Storchaka、Eryk Sun、Chad Whitacre、Graham Wideman。