python / 3.7.2rc1 / all / howto-unicode.html

Unicode HOWTO

  • Release

    • 1.12

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

Unicode 简介

Definitions

当今的程序需要能够处理各种各样的字符。应用程序经常被国际化,以各种用户可选语言显示消息和输出。同一程序可能需要输出英语,法语,日语,希伯来语或俄语的错误消息。 Web 内容可以用任何一种语言编写,也可以包含各种表情符号。 Python 的字符串类型使用 Unicode 标准表示字符,这使 Python 程序可以使用所有这些可能的字符。

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

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

Unicode 标准描述了如何pass 代码点 来表示字符。代码点值是 0 到 0x10FFFF 范围内的整数(大约 110 万个值,到目前为止已分配了 11 万个)。在标准和本文档中,使用符号U+265E编写代码点,以表示值为0x265e的字符(十进制为 9822)。

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是一个代码点,代表某些特定字符;在这种情况下,它表示字符“ BLACK CHESS KNIGHT”,“♞”。在非正式情况下,有时会忘记代码点和字符之间的区别。

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

Encodings

总结上一节:Unicode 字符串是代码点的序列,它们是从 0 到0x10FFFF的数字(十进制 1,114,111)。这些代码点序列需要在内存中表示为一组“代码单元” ,然后将“代码单元 ”Map 到 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

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

  • 它不是便携式的;不同的处理器对字节的排序不同。

  • 这是非常浪费的空间。在大多数文本中,大多数代码点小于 127,或小于 255,因此0x00个字节占用了大量空间。上面的字符串占用 24 个字节,而 ASCII 表示形式则需要 6 个字节。增加的 RAM 使用量并不太重要(台式计算机具有 GB 的 RAM,并且字符串通常不会那么大),但是将磁盘和网络带宽的使用扩展 4 倍是无法忍受的。

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

因此,这种编码的使用率不是很高,人们会选择其他更有效,更方便的编码,例如 UTF-8.

UTF-8 是最常用的编码之一,Python 通常默认使用它。 UTF 代表“ Unicode 转换格式”,“ 8”表示在编码中使用 8 位值。 (也有 UTF-16 和 UTF-32 编码,但使用频率比 UTF-8 少.)UTF-8 使用以下规则:

  • 如果代码点<128,则由相应的字节值表示。

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

UTF-8 具有几个方便的属性:

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

  • Unicode 字符串被转换为一个字节序列,该字节序列仅包含嵌入式零字节(它们表示空字符(U 0000))。这意味着 UTF-8 字符串可以由strcpy()之类的 C 函数处理,并pass协议处理,该协议不能处理零字节,而字符串结束标记除外。

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

  • UTF-8 非常紧凑;大多数常用字符可以用一个或两个字节表示。

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

  • UTF-8 是面向字节的编码。编码指定每个字符由一个或多个字节的特定序列表示。这就避免了整数和面向字的编码(如 UTF-16 和 UTF-32)可能发生的字节排序问题,其中字节的 Sequences 根据对字符串进行编码的硬件而有所不同。

References

Unicodeunion 网站具有字符表,词汇表和 Unicode 规范的 PDF 版本。为阅读困难做好准备。该站点上还提供 Unicode 起源和 Developing 的A chronology

在 Computerphile YoutubeChannels 上,Tom Scott 简短地讨论 Unicode 和 UTF-8 的历史(9 分 36 秒)。

为了帮助理解该标准,Jukka Korpela 已编写入门指南来读取 Unicode 字符表。

另一个好的入门文章由 Joel Spolsky 写。如果此简介不能使您理解清楚,则应在 continue 之前try阅读本替代文章。

维基百科条目通常很有帮助;例如,请参见“ character encoding”和UTF-8的条目。

Python 的 Unicode 支持

既然您已经了解了 Unicode 的基础知识,我们就可以看看 Python 的 Unicode Function。

字符串类型

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

Python 源代码的默认编码为 UTF-8,因此您只需在字符串 Literals 中包含 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")

如果由于某种原因您不能在编辑器中 Importing 特定字符或由于某种原因而希望仅保留源代码 ASCII,则还可以在字符串 Literals 中使用转义序列。 (根据您的系统,您可能会看到实际的大写字母三角字形,而不是 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'

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

  • errors *参数指定当无法根据编码规则转换 Importing 字符串时的响应。此参数的合法值为'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 种不同的编码。有关列表,请参见Standard Encodings处的 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表示形式,并以请求的* encoding *进行编码。

  • errors *参数与decode()方法的参数相同,但支持更多可能的处理程序。除了'strict''ignore''replace'(在这种情况下,插入一个问号而不是无法编码的字符)之外,还有'xmlcharrefreplace'(插入 XML 字符引用),backslashreplace(插入\uNNNN转义序列)和namereplace(插入 a) \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模块。但是,此模块返回的编码和解码Function通常比舒适度更底层,并且编写新的编码是一项专门的任务,因此本 HOWTO 不会涵盖该模块。

Python 源代码中的 UnicodeLiterals

在 Python 源代码中,可以使用\u转义序列编写特定的 Unicode 代码点,其后是四个十六进制数字来表示代码点。 \U转义序列相似,但是期望使用 8 个十六进制数字,而不是 4 个:

>>> 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()内置函数来汇编字符串,但这更加乏味。

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

Python 默认情况下支持使用 UTF-8 编写源代码,但是如果语句正在使用的编码,则几乎可以使用任何编码。这可以pass在源文件的第一行或第二行中添加特殊 Comments 来完成:

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

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

该语法受 Emacs 用于指定文件本地变量的符号的启发。 Emacs 支持许多不同的变量,但是 Python 仅支持“编码”。 -*-符号向 Emacs 表示 Comments 是特殊的;它们对 Python 没有意义,只是一个约定。 Python 在 Comments 中查找coding: namecoding=name

如果您不包含此类 Comments,则如上所述,默认编码将为 UTF-8.另请参见 PEP 263

Unicode Properties

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'表示“ Number,other”,'Mn'是“ Mark,nonspacing”,'So'是“ Symbol,other”。有关类别代码的列表,请参见Unicode 字符数据库文档的“常规类别值”部分

Comparing Strings

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

一种不区分大小写的比较工具是casefold() string 方法,该方法按照 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))

运行时,输出:

$ python3 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 字符,但仅[a-zA-Z0-9_]以字节为单位,或者如果提供了re.ASCII,则\s将匹配 Unicode 空格字符或[ \t\n\r\f\v]

References

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

Literals 序列类型-str的 Python 库参考中描述了str类型。

unicodedata模块的文档。

codecs模块的文档。

Marc-AndréLemburg 在 EuroPython 2002 上给出了名为“ Python 和 Unicode”的演示文稿(PDF 幻灯片)。幻灯片很好地概述了 Python 2 的 Unicode Function的设计(其中 Unicode 字符串类型称为unicode,而 Literals 以u开头)。

读写 Unicode 数据

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

根据 Importing 源和输出目的地,可能不需要执行任何操作。您应该检查应用程序中使用的库是否本机支持 Unicode。例如,XML 解析器通常返回 Unicode 数据。许多关系数据库还支持 Unicode 值列,并且可以从 SQL 查询返回 Unicode 值。

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

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

解决方案是使用低级解码接口来捕获部分编码序列的情况。实现此Function的工作已经为您完成:内置的open()函数可以返回类似文件的对象,该对象假定文件的内容采用指定的编码,并接受read()write()等方法的 Unicode 参数。这passopen()的* encoding errors *参数起作用,这些参数的解释方式与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用作字节 Sequences 标记(BOM),通常被写为文件的第一个字符,以帮助自动检测文件的字节 Sequences。某些编码(例如 UTF-16)期望 BOM 表出现在文件的开头;当使用这种编码时,BOM 将自动作为第一个字符写入,并且在读取文件时将被静默删除。这些编码有多种变体,例如用于 Little-endian 和 Big-endian 编码的'utf-16-le'和'utf-16-be',它们指定一个特定的字节 Sequences,并且不会跳过 BOM。

在某些 locale,在 UTF-8 编码文件的开头还使用“ BOM”是惯例。该名称具有误导性,因为 UTF-8 与字节 Sequences 无关。标记只是宣布文件已以 UTF-8 编码。要读取此类文件,请使用“ utf-8-sig”编解码器自动跳过标记(如果存在)。

Unicode filenames

今天,大多数常用的 os 都支持包含任意 Unicode 字符的文件名。通常,这是pass将 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 版本。

请注意,在大多数情况下,您应该坚持将 Unicode 与这些 API 一起使用。字节 API 仅应在存在无法解码文件名的系统上使用;现在几乎只有 Unix 系统。

编写支持 Unicode 的程序的技巧

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

最重要的提示是:

Note

软件仅应在内部使用 Unicode 字符串,尽快对 Importing 数据进行解码,并仅在最后对输出进行编码。

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

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

在文件编码之间转换

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

例如,如果您有一个 Importing 文件* f *位于 Latin-1 中,则可以用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错误处理程序对数据进行编码并将其写回时,这些代码点将转回相同的字节。

References

David Beazley 在 PyCon 2010 上的演讲掌握 Python 3Importing/输出的一部分讨论了文本处理和二进制数据处理。

Marc-AndréLemburg 的演示文稿“用 Python 编写支持 Unicode 的应用程序”的 PDF 幻灯片讨论字符编码问题以及如何对应用程序进行国际化和本地化。这些幻灯片仅涵盖 Python2.x。

Python 中的 Unicode 胆量是 Benjamin Peterson 在 PyCon 2013 上的演讲,讨论了 Python 3.3 中的内部 Unicode 表示形式。

Acknowledgements

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

感谢以下在本文中指出错误或提出建议的人员:埃里克·阿劳霍,尼古拉斯·巴斯汀,尼克·科格兰,马里乌斯·吉德米纳斯,肯特·约翰逊,肯·克鲁格勒,马克·安德烈·伦伯格,马丁·冯·洛维斯,特里·J·里迪,谢里·斯托查卡,Eryk Sun,Chad Whitacre,Graham Wideman。