9.4. Unicode

Unicode 是一种用于表示世界上所有不同语言字符的系统。当 Python 解析 XML 文档时,所有数据都以 Unicode 格式存储在内存中。

我们稍后会详细讨论,但首先,让我们先了解一些背景知识。

**历史注释。** 在 Unicode 出现之前,每种语言都有各自独立的字符编码系统,每个系统都使用相同的数字(0-255)来表示该语言的字符。有些语言(如俄语)在如何表示相同的字符方面存在多种相互冲突的标准;而另一些语言(如日语)则拥有非常多的字符,以至于需要使用多字节字符集。在不同系统之间交换文档非常困难,因为计算机无法确定文档作者使用了哪种字符编码方案;计算机只能看到数字,而这些数字可能代表不同的含义。然后,想象一下尝试将这些文档存储在同一个地方(例如同一个数据库表中);您需要将字符编码与每段文本一起存储,并在传递文本时确保一并传递编码信息。再想象一下多语言文档,同一个文档中包含来自多种语言的字符。(它们通常使用转义码来切换模式;例如,您现在处于俄语 koi8-r 模式,因此字符 241 表示这个意思;然后,您切换到 Mac 希腊语模式,字符 241 就表示其他意思。以此类推。)Unicode 的设计就是为了解决这些问题。

为了解决这些问题,Unicode 使用 0 到 65535 之间的 2 字节数字来表示每个字符。[5] 每个 2 字节数字代表世界上至少有一种语言使用的唯一字符。(在多种语言中使用的字符具有相同的数字代码。)每个字符只有一个数字,每个数字也只对应一个字符。Unicode 数据永远不会产生歧义。

当然,所有这些遗留编码系统的问题仍然存在。例如,7 位 ASCII 使用 0 到 127 之间的数字来存储英文字符。(65 是大写字母“A”,97 是小写字母“a”,等等。)英语的字母表非常简单,因此可以用 7 位 ASCII 完全表示。法语、西班牙语和德语等西欧语言都使用一种称为 ISO-8859-1(也称为“latin-1”)的编码系统,该系统使用 7 位 ASCII 字符表示 0 到 127 之间的数字,然后扩展到 128-255 范围,用于表示带有波浪号的 n (241) 和带有两个点的 u (252) 等字符。Unicode 使用与 7 位 ASCII 相同的字符表示 0 到 127,使用与 ISO-8859-1 相同的字符表示 128 到 255,然后从那里扩展到使用剩余数字(256 到 65535)表示其他语言的字符。

在处理 Unicode 数据时,您有时可能需要将数据转换回其中一种遗留编码系统。例如,为了与其他期望使用特定 1 字节编码方案的计算机系统集成,或者为了将其打印到不支持 Unicode 的终端或打印机上。或者将其存储在明确指定编码方案的 XML 文档中。

说到这里,让我们回到 Python

从 2.0 版本开始,Python 就开始在整个语言中支持 Unicode。 XML 包使用 Unicode 存储所有解析的 XML 数据,但您可以在任何地方使用 Unicode。

示例 9.13. Unicode 简介

>>> s = u'Dive in'            1
>>> s
u'Dive in'
>>> print s                   2
Dive in
1 要创建 Unicode 字符串而不是常规的 ASCII 字符串,请在字符串前面添加字母“u”。请注意,此特定字符串不包含任何非 ASCII 字符。这没关系;Unicode 是 ASCII 的超集(一个非常大的超集),因此任何常规的 ASCII 字符串也可以存储为 Unicode。
2 在打印字符串时,Python 会尝试将其转换为默认编码,通常是 ASCII。(稍后会详细介绍。)由于此 Unicode 字符串由也是 ASCII 字符的字符组成,因此打印它的结果与打印常规的 ASCII 字符串相同;转换是无缝的,如果您不知道 s 是 Unicode 字符串,您永远不会注意到其中的区别。

示例 9.14. 存储非 ASCII 字符

>>> s = u'La Pe\xf1a'         1
>>> print s                   2
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> print s.encode('latin-1') 3
La Peña
1 当然,Unicode 的真正优势在于它能够存储非 ASCII 字符,例如西班牙语中的“ñ”(带有波浪号的 n)。波浪号 n 的 Unicode 字符代码是十六进制的 0xf1(十进制的 241),您可以像这样输入:\xf1
2 还记得我说过,print 函数会尝试将 Unicode 字符串转换为 ASCII,以便打印它吗?嗯,这在这里行不通,因为您的 Unicode 字符串包含非 ASCII 字符,因此 Python 会引发 UnicodeError 错误。
3 这就是从 Unicode 转换为其他编码方案的用武之地。s 是一个 Unicode 字符串,但 print 只能打印常规字符串。为了解决这个问题,您可以调用每个 Unicode 字符串上都有的 encode 方法,将 Unicode 字符串转换为给定编码方案中的常规字符串,您需要将编码方案作为参数传递。在本例中,您使用的是 latin-1(也称为 iso-8859-1),它包含波浪号 n(而默认的 ASCII 编码方案不包含,因为它只包含编号为 0 到 127 的字符)。

还记得我说过,每当 Python 需要从 Unicode 字符串生成常规字符串时,它通常会将 Unicode 转换为 ASCII 吗?嗯,这个默认编码方案是一个您可以自定义的选项。

示例 9.15. sitecustomize.py

# sitecustomize.py                   1
# this file can be anywhere in your Python path,
# but it usually goes in ${pythondir}/lib/site-packages/
import sys
sys.setdefaultencoding('iso-8859-1') 2
1 sitecustomize.py 是一个特殊的脚本;Python 会在启动时尝试导入它,因此其中的任何代码都会自动运行。正如注释中提到的,它可以放在任何地方(只要 import 可以找到它),但它通常放在 Python lib 目录下的 site-packages 目录中。
2 setdefaultencoding 函数用于设置默认编码。这是 Python 在需要将 Unicode 字符串自动强制转换为常规字符串时尝试使用的编码方案。

示例 9.16. 设置默认编码的效果

>>> import sys
>>> sys.getdefaultencoding() 1
'iso-8859-1'
>>> s = u'La Pe\xf1a'
>>> print s                  2
La Peña
1 本示例假设您已对 sitecustomize.py 文件进行了上一个示例中列出的更改,并重新启动了 Python。如果您的默认编码仍然显示为 'ascii',则说明您没有正确设置 sitecustomize.py,或者您没有重新启动 Python。默认编码只能在 Python 启动期间更改;您无法在以后更改它。(由于我不想现在就深入讨论的一些古怪的编程技巧,您甚至无法在 Python 启动后调用 sys.setdefaultencoding。深入研究 site.py 并搜索“setdefaultencoding”以了解原因。)
2 现在,默认编码方案包含了您在字符串中使用的所有字符,Python 可以毫无问题地自动强制转换字符串并打印它。

示例 9.17. 在 .py 文件中指定编码

如果您要在 Python 代码中存储非 ASCII 字符串,则需要通过在每个文件的顶部放置一个编码声明来指定每个 .py 文件的编码。此声明将 .py 文件定义为 UTF-8

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

现在,XML 呢?嗯,每个 XML 文档都使用特定的编码。同样,ISO-8859-1 是一种流行的西欧语言数据编码。KOI8-R 在俄语文本中很流行。如果指定了编码,则编码位于 XML 文档的标头中。

示例 9.18. russiansample.xml


<?xml version="1.0" encoding="koi8-r"?>       1
<preface>
<title>Предисловие</title>                    2
</preface>
1 这是一个真实的俄语 XML 文档的示例摘录;它是本书俄语翻译的一部分。请注意标头中指定的编码 koi8-r
2 这些是西里尔字母,据我所知,它们拼写的是俄语单词“前言”。如果您在常规文本编辑器中打开此文件,这些字符很可能会显示为乱码,因为它们使用 koi8-r 编码方案进行编码,但以 iso-8859-1 显示。

示例 9.19. 解析 russiansample.xml

>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('russiansample.xml') 1
>>> title = xmldoc.getElementsByTagName('title')[0].firstChild.data
>>> title                                       2
u'\u041f\u0440\u0435\u0434\u0438\u0441\u043b\u043e\u0432\u0438\u0435'
>>> print title                                 3
Traceback (innermost last):
  File "<interactive input>", line 1, in ?
UnicodeError: ASCII encoding error: ordinal not in range(128)
>>> convertedtitle = title.encode('koi8-r')     4
>>> convertedtitle
'\xf0\xd2\xc5\xc4\xc9\xd3\xcc\xcf\xd7\xc9\xc5'
>>> print convertedtitle                        5
Предисловие
1 我在这里假设您已将上一个示例保存为当前目录中的 russiansample.xml。为了完整起见,我还假设您已通过删除 sitecustomize.py 文件或至少注释掉 setdefaultencoding 行将默认编码改回 'ascii'
2 请注意,title 标记的文本数据(现在位于 title 变量中,这要归功于我草率跳过并且令人讨厌的是直到下一节才会解释的 Python 函数的长串连接)——XML 文档的 title 元素内的文本数据以 Unicode 存储。
3 无法打印标题,因为此 Unicode 字符串包含非 ASCII 字符,因此 Python 无法将其转换为 ASCII,因为这没有意义。
4 但是,您可以将其显式转换为 koi8-r,在这种情况下,您将获得一个(常规的,非 Unicode)单字节字符字符串(f0d2c5 等),它们是原始 Unicode 字符串中字符的 koi8-r 编码版本。
5 打印 koi8-r 编码的字符串可能会在您的屏幕上显示乱码,因为您的 Python IDE 将这些字符解释为 iso-8859-1,而不是 koi8-r。但至少它们确实打印出来了。(而且,如果您仔细观察,就会发现这与您在不支持 Unicode 的文本编辑器中打开原始 XML 文档时看到的乱码相同。Python 在解析 XML 文档时将其从 koi8-r 转换为 Unicode,而您只是将其转换回去了。)

总而言之,如果您以前从未见过 Unicode,它本身可能会有点吓人,但在 Python 中处理 Unicode 数据实际上非常容易。如果您的 XML 文档都是 7 位 ASCII(如本章中的示例),您实际上永远不会想到 Unicode。Python 会在解析时将 XML 文档中的 ASCII 数据转换为 Unicode,并在必要时自动将其强制转换回 ASCII,而您甚至不会注意到。但如果您需要用其他语言处理它,Python 也已准备就绪。

延伸阅读

脚注

[5] 遗憾的是,这仍然过于简单了。Unicode 现在已经扩展到可以处理古代汉语、韩语和日语文本,这些文本有太多不同的字符,以至于 2 字节的 Unicode 系统无法全部表示。但是 Python 目前不支持开箱即用的功能,而且我不知道是否有正在进行的项目来添加它。很抱歉,这已经超出了我的专业知识范围。