Skip to main content

Unicode HOWTO

Release:

1.12

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

Unicode简介

字符代码的历史

在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的值.ASC码只能达到127,所以一些机器在128和255之间赋予重音字符。然而,不同的机器具有不同的代码,这导致交换文件时出现问题。最终,出现了128–255范围的各种常用的值集合。一些是由国际标准组织定义的真正标准,一些是由一个公司或另一个公司发明的并且设法赶上的 事实上 公约。

255个字符不是很多。例如,您不能将西欧使用的重音字符和用于俄语的西里尔字母添加到128–255范围,因为有超过128个这样的字符。

你可以使用不同的代码编写文件(所有的俄语文件在一个称为KOI8的编码系统,所有的法语文件在一个不同的编码系统Latin1),但如果你想写一个法国文件,引用一些俄罗斯文本?在20世纪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最初是单独的努力,但规范与1.1版本的Unicode合并。

(这个对Unicode的历史的讨论是高度简化的。精确的历史细节对于了解如何有效地使用Unicode没有必要,但如果你好奇,请参考参考文献或 Unicode的维基百科条目 中列出的Unicode联盟网站了解更多信息。

定义

字符 是文本的最小可能组件。 ‘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使用情况并不重要太多(台式计算机具有千兆字节的RAM,并且字符串通常不大),但是将磁盘和网络带宽的使用扩展到4倍是不能容忍的。

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

  4. 许多互联网标准是根据文本数据定义的,并且不能处理具有嵌入的零字节的内容。

通常人们不使用这种编码,而是选择更高效和方便的其他编码。 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大型机上使用IBM的EBCDIC。字母值不在一个块中:“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!' 创建的任何字符串或三引号字符串语法存储为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")

如果您无法在编辑器中输入特定字符,或者希望保留源代码(仅限某些原因),则还可以在字符串文字中使用转义序列。 (根据您的系统,您可能会看到实际的资本 - 增量字形,而不是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 参数指定无法根据编码规则转换输入字符串时的响应。此参数的合法值为 '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‘都是同一编码的同义词。

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

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

转换为字节

bytes.decode() 的相反方法是 str.encode(),它返回在请求的 encoding 中编码的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源代码中,特定的Unicode代码点可以使用 \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规范包括关于代码点的信息的数据库。对于每个定义的代码点,信息包括字符的名称,其类别,数字值(如果适用的话,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' 表示“Letter,小写”,'No' 表示“Number,other”,'Mn' 是“Mark,nonspacing”,'So' 是“Symbol,other”。有关类别代码的列表,请参阅 Unicode字符数据库文档的常规类别值部分

Unicode正则表达式

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]

参考文献

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

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

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字符串转换为根据系统而变化的某种编码来实现的。例如,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 + bytesTypeError 将被提出。

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

在文件编码之间转换

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

例如,如果您有一个拉丁语-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 错误处理程序时,这些专用代码点将被转回相同的字节。

参考文献

掌握Python 3输入/输出 的一部分,David Beazley的PyCon 2010谈话,讨论文本处理和二进制数据处理。

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

Unicode在Python中的胆量 是Benjamin Peterson的PyCon 2013讨论,讨论Python 3.3中的内部Unicode表示。

致谢

本文档的初稿由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 。