Unicode HOWTO

发布:1.12

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

Introduction to Unicode

History of Character Codes

在1968年,美国信息交换标准代码,以其首字母缩写ASCII更好地被称为标准化。用于各种字符的ASCII定义数字代码,数字值从0到127。例如,小写字母“a”被分配97作为其代码值。

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

有一段时间,人们只是写了没有显示重音的程序。在20世纪80年代中期,由法语发言者编写的一个Apple II BASIC程序可能有这样的行:

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

这些消息应该包含重音(terminée,paramètre,enregistrés),他们只是看到错误的人谁可以阅读法语。

在20世纪80年代,几乎所有的个人计算机都是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联盟网站或Wikipedia条目Unicode以获取更多信息)。

Definitions

字符是文本的最小可能组件。'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工具包或终端的字体渲染器的工作。

Encodings

总结前面的部分:Unicode字符串是一系列代码点,它们是从0到0x10FFFF(1,114,111十进制)的数字。该序列需要被表示为存储器中的一组字节(意思是,从0到255的值)。将Unicode字符串转换为字节序列的规则称为encoding

你可能想到的第一个编码是一个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()不兼容,因此需要使用一个新的宽字符串函数系列。
  4. 许多互联网标准是根据文本数据定义的,并且不能处理具有嵌入的零字节的内容。

通常人们不使用这种编码,而是选择更有效和方便的其他编码。UTF-8可能是最常支持的编码;将在下面讨论。

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

  1. 如果代码点是
  2. 如果代码点为128或更大,则无法在此编码中表示Unicode字符串。(在这种情况下,Python引发a UnicodeEncodeError异常)。

拉丁语-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. 如果代码点是
  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。

References

Unicode Consortium网站具有Unicode规范的字符图表,词汇表和PDF版本。准备一些困难的阅读。也可以在网站上获得Unicode的起源和发展的年表

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

另一个好的介绍性文章由Joel Spolsky编写。如果这篇介绍没有使你明白,你应该继续阅读这篇替代文章。

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

Python’s Unicode Support

现在你已经学习了Unicode的基本原理,我们可以看看Python的Unicode特性。

The String Type

从Python 3.0开始,该语言具有str类型,其中包含Unicode字符,表示使用“unicode rocks!” / t3>,'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é")

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

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

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

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

如果您无法在编辑器中输入特定字符,或者希望保留源代码(仅限某些原因),则还可以在字符串字面值中使用转义序列。(根据您的系统,您可能会看到实际的资本 - 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'

此外,可以使用bytesdecode()方法创建字符串。此方法采用编码参数,例如UTF-8和可选的错误参数。

errors参数指定当输入字符串无法根据编码规则进行转换时的响应。Legal values for this argument are 'strict' (raise a UnicodeDecodeError exception), 'replace' (use U+FFFD, REPLACEMENT CHARACTER), 'ignore' (just leave the character out of the Unicode result), or 'backslashreplace' (inserts a \xNN escape sequence). 以下示例显示了差异:

>>> 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种不同的编码;有关列表,请参阅Standard Encodings上的Python库参考。一些编码有多个名称;例如,'latin-1''iso_8859_1''8859'都是同一编码的同义词。

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

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

Converting to Bytes

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。

Unicode Literals in Python Source Code

在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在注释中寻找编码: namecoding=name

如果你不包括这样的注释,使用的默认编码将是已经提到的UTF-8。有关详细信息,请参阅 PEP 263

Unicode Properties

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

类别代码是描述字符性质的缩写。这些被分组成诸如“字母”,“数字”,“标点”或“符号”的类别,其又被分成子类别。To take the codes from the above output, 'Ll' means ‘Letter, lowercase’, 'No' means “Number, other”, 'Mn' is “Mark, nonspacing”, and 'So' is “Symbol, other”. 有关类别代码的列表,请参见Unicode字符数据库文档的常规类别值部分。

Unicode Regular Expressions

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

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

import re
p = re.compile('\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_]以字节或如果re.ASCII\s将匹配Unicode空格字符或[ \ t \ n \ r \ f \ v] / t9>。

References

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

str类型在Python库引用的Text Sequence Type — str中进行了描述。

unicodedata模块的文档。

codecs模块的文档。

Marc-André Lemburg gave a presentation titled “Python and Unicode” (PDF slides) at EuroPython 2002. 幻灯片是对Python 2的Unicode特性(其中Unicode字符串类型称为unicode和字面值以u开头)的设计的良好概述。

Reading and Writing Unicode Data

一旦你写了一些与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()This works through open()‘s encoding and errors parameters which are interpreted just like those in str.encode() and 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 filenames

目前常用的大多数操作系统支持包含任意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系统。

Tips for Writing Unicode-aware Programs

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

最重要的提示是:

Software should only work with Unicode strings internally, decoding the input data as soon as possible and encoding the output only at the end.

如果你试图编写接受Unicode和字节字符串的处理函数,你会发现你的程序容易受到错误,无论你组合两种不同类型的字符串。没有自动编码或解码:如果你这样做。str + 字节,将会出现TypeError

如果你这样做,小心检查解码的字符串,而不是编码的字节数据;一些编码可能具有有趣的属性,例如不是双射的或不是完全ASCII兼容的。如果输入数据也指定了编码,那么尤其如此,因为攻击者可以选择一种聪明的方式来在编码的字节流中隐藏恶意文本。

Converting Between File Encodings

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

例如,如果您有一个输入文件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') )

Files in an Unknown Encoding

如果您需要更改文件,但不知道文件的编码,您该怎么办?如果知道编码是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错误处理程序时,这些专用代码点将被转回相同的字节。

References

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

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

Python在Python中的胆量是本杰明·佩特森讨论Python 3.3中的内部Unicode表示的PyCon 2013谈话。

Acknowledgements

本文档的初稿由Andrew Kuchling撰写。它后来被Alexander Belopolsky,Georg Brandl,Andrew Kuchling和Ezio Melotti进一步修订。

感谢以下人士在本文中注意到错误或提出建议:ÉricAraujo,Nicholas Bastin,Nick Coghlan,Marius Gedminas,Kent Johnson,Ken Krugler,Marc-AndréLemburg,Martin vonLöwis,Terry J. Reedy,Chad Whitacre 。