Unicode WHOTO

释放

1.12

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

Unicode简介

定义

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

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

A character 是文本中可能的最小组件。”A、B、C等都是不同的字符。“_”和“_”也一样。根据所说的语言或上下文,字符会有所不同。例如,“罗马数字1”有一个字符,“i”,它与大写字母“i”分开。它们通常看起来是一样的,但这是两个含义不同的字符。

Unicode标准描述了字符的表示方式 代码点 . 代码点值是一个介于0到0x10FFFF之间的整数(大约110万个值 actual number assigned 比这还少)。在标准和本文档中,使用符号编写代码点 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 是一个代码点,代表某个特定的字符;在本例中,它代表字符“黑棋骑士”,“”。在非正式的上下文中,有时会忘记代码点和字符之间的区别。

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

编码

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

您可能会想到的第一种编码是使用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 字节。与ASCII表示所需的6个字节相比,上面的字符串需要24个字节。增加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 Consortium site 具有Unicode规范的字符图、词汇表和PDF版本。为一些难懂的读物做好准备。 A chronology 网站上也提供了Unicode的起源和发展。

在computerphile youtube频道上,Tom Scott简短地说 discusses the history of Unicode and UTF-8 (9分36秒)。

为了帮助理解标准,Jukka Korpela写了 an introductory guide 读取Unicode字符表。

另一 good introductory article 是乔尔·斯波斯基写的。如果这篇介绍没有让你明白,在继续之前,你应该先阅读这篇替代文章。

维基百科条目通常很有用;请参见“character encoding<https://en.wikipedia.org/wiki/character_encoding>”和 UTF-8 例如。

python的unicode支持

既然您已经了解了Unicode的基本知识,那么我们可以看看Python的Unicode特性。

String Type

从Python3.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种不同的编码;请参见 标准编码 一个列表。有些编码有多个名称;例如, 'latin-1''iso_8859_1''8859 '都是相同编码的同义词。

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

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

转换为字节

相反的方法 bytes.decode()str.encode() ,返回 bytes Unicode字符串的表示形式,在请求的 encoding .

这个 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 模块。然而,这个模块返回的编码和解码功能通常比舒适性更低级,编写新的编码是一个专门的任务,因此本指南不涉及这个模块。

python源代码中的Unicode文本

在Python源代码中,可以使用 \u 转义序列,后跟四个十六进制数字,给出代码点。这个 \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规范包括有关代码点的信息数据库。对于每个定义的代码点,信息包括字符的名称、其类别、数值(如果适用)(对于表示数字概念的字符,如罗马数字、分数,如三分之一和五分之四等)。还有与显示相关的属性,例如如何在双向文本中使用代码点。

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

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' 是“符号,其他”。见 the General Category Values section of the Unicode Character Database documentation 类别代码列表。

比较字符串

Unicode为比较字符串增加了一些复杂性,因为相同的字符集可以由不同的代码点序列表示。例如,类似“_”的字母可以表示为单个代码点U+00e a,也可以表示为U+0065 U+0302,它是“e”的代码点,后面是“组合扬抑符”的代码点。它们在打印时将产生相同的输出,但一个是长度为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))

运行时,此输出:

$ 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+ 将匹配泰国数字并打印出来。如果你提供 re.ASCII 旗到 compile()\d+ 将与子字符串“57”匹配。

同样地, \w 匹配多种Unicode字符,但仅匹配 [a-zA-Z0-9_] 以字节或if为单位 re.ASCII 提供,以及 \s 将匹配Unicode空白字符或 [ \t\n\r\f\v] .

工具书类

关于python的unicode支持的一些好的替代讨论是:

  • Processing Text Files in Python 3 <http://python-notes.curiousefficiency.org/en/latest/python3/text_file_processing.html> _尼克·科格伦。

  • Pragmatic Unicode <https://nedbatchelder.com/text/unipain.html> _ Ned Batchelder的Pycon 2012演讲。

这个 str 类型在python库参考中描述,位于 文本序列类型--- str .

文件 unicodedata 模块。

文件 codecs 模块。

Marc Andr_Lemburg给出 a presentation titled "Python and Unicode" (PDF slides) 2002年在欧洲。这些幻灯片非常全面地介绍了Python2的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() .这是通过 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,而Python3.6在Windows上也切换到使用UTF-8。在Unix系统上,将只有一个 filesystem encoding 。如果您已将 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字符串列表,而传递字节路径将以字节形式返回文件名。例如,假设默认设置为 filesystem encoding 是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的软件的建议。

最重要的提示是:

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

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

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

在文件编码之间转换

这个 StreamRecoder 类可以透明地在编码之间进行转换,获取一个以编码方式返回数据的流1,其行为类似于以编码方式返回数据的流2。

例如,如果您有一个输入文件 f 那是拉丁语-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 错误处理程序用于对数据进行编码并将其写回。

工具书类

一段 Mastering Python 3 Input/Output 大卫·比兹利(DavidBeazley)在2010年的Pycon演讲中讨论了文本处理和二进制数据处理。

这个 PDF slides for Marc-André Lemburg's presentation "Writing Unicode-aware Applications in Python" 讨论字符编码问题以及如何使应用程序国际化和本地化。这些幻灯片仅包含python 2.x。

The Guts of Unicode in Python 是BenjaminPeterson在2013年的一次Pycon演讲,讨论了Python3.3中的内部Unicode表示。

确认

这份文件的初稿是安德鲁·库克林写的。此后,亚历山大·伯罗波尔斯基、乔治·布兰德、安德鲁·库奇林和埃齐奥·梅洛蒂进一步修订了这本书。

感谢以下指出本文错误或提出建议的人:Ric Araujo、Nicholas Bastin、Nick Coghlan、Marius Gedminas、Kent Johnson、Ken Krugler、Marc Andr_Lemburg、Martin von L_wis、Terry J.Reedy、Serhiy Storchaka、Erik Sun、Chad Whitacre、Graham Wideman。