Unicode HOWTO — Python 文档

来自菜鸟教程
Python/docs/2.7/howto/unicode
跳转至:导航、​搜索

Unicode HOWTO

发布
1.03

本 HOWTO 讨论了 Python 2.x 对 Unicode 的支持,并解释了人们在尝试使用 Unicode 时经常遇到的各种问题。 对于 Python 3 版本,请参见 < https://docs.python.org/3/howto/unicode.html >.

Unicode 简介

字符代码的历史

1968 年,美国信息交换标准代码(以其首字母缩写词 ASCII 更为人所知)被标准化。 ASCII 定义了各种字符的数字代码,数值从 0 到 127。 例如,小写字母“a”被指定为 97 作为其代码值。

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

有一段时间,人们只是编写不显示重音的程序。 我记得看过 Apple ][ BASIC 程序,该程序于 1980 年代中期在法语出版物中发表,其中有如下几行:

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

这些消息应该包含口音,对于会读法语的人来说,它们看起来是错误的。

在 1980 年代,几乎所有的个人计算机都是 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(base-16 中的 0x10ffff)。

有一个相关的 ISO 标准 ISO 10646。 Unicode 和 ISO 10646 最初是分开的工作,但规范与 Unicode 的 1.1 修订版合并。

(此对 Unicode 历史的讨论已高度简化。 我认为普通的 Python 程序员不需要担心历史细节; 有关更多信息,请查阅参考资料中列出的 Unicode 联盟站点。)


定义

character 是文本的最小可能组成部分。 'A'、'B'、'C' 等都是不同的字符。 'È' 和 'Í' 也是如此。 字符是抽象的,会因您所谈论的语言或上下文而异。 例如,欧姆 (Ω) 的符号通常与希腊字母表中的大写字母 omega (Ω) 非常相似(它们甚至在某些字体中可能相同),但这是两个具有不同含义的不同字符。

Unicode 标准描述了字符如何由 码位 表示。 代码点是一个整数值,通常以 16 进制表示。 在标准中,代码点使用符号 U+12ca 来表示值为 0x12ca(4810 十进制)的字符。 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 的数字。 该序列需要在内存中表示为一组字节(即 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,因此大量空间被零字节占用。 与 ASCII 表示所需的 6 个字节相比,上述字符串需要 24 个字节。 RAM 使用量的增加并不重要(台式计算机有 MB 的 RAM,而字符串通常不会那么大),但是将我们对磁盘和网络带宽的使用量增加 4 倍是无法忍受的。
  3. 它与现有的 C 函数(如 strlen())不兼容,因此需要使用新的宽字符串函数系列。
  4. 许多 Internet 标准是根据文本数据定义的,无法处理嵌入零字节的内容。

一般人们不使用这种编码,而是选择其他更高效、更方便的编码。 UTF-8 可能是最常支持的编码; 下文将对此进行讨论。

编码不必处理每一个可能的 Unicode 字符,而且大多数编码都不需要。 例如,Python 的默认编码是“ascii”编码。 将 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 的 EBCDIC,它用于 IBM 大型机。 字母值不在一个块中:“a”到“i”的值为 129 到 137,但“j”到“r”的值为 145 到 153。 如果您想使用 EBCDIC 作为编码,您可能会使用某种查找表来执行转换,但这主要是内部细节。

UTF-8 是最常用的编码之一。 UTF 代表“Unicode 转换格式”,'8' 表示编码中使用 8 位数字。 (也有 UTF-16 编码,但使用频率低于 UTF-8。)UTF-8 使用以下规则:

  1. 如果代码点 <128,则由相应的字节值表示。
  2. 如果代码点在 128 和 0x7ff 之间,它会变成 128 和 255 之间的两个字节值。
  3. 代码点 >0x7ff 被转换为三或四字节序列,其中序列的每个字节在 128 到 255 之间。

UTF-8 有几个方便的特性:

  1. 它可以处理任何 Unicode 代码点。
  2. Unicode 字符串被转换为不包含嵌入零字节的字节字符串。 这避免了字节顺序问题,并且意味着 UTF-8 字符串可以由 C 函数(如 strcpy())处理并通过无法处理零字节的协议发送。
  3. 一串 ASCII 文本也是有效的 UTF-8 文本。
  4. UTF-8 相当紧凑; 大多数代码点变成了两个字节,小于 128 的值只占用一个字节。
  5. 如果字节损坏或丢失,则可以确定下一个 UTF-8 编码代码点的开始并重新同步。 随机 8 位数据也不太可能看起来像有效的 UTF-8。


参考

Unicode 联盟网站位于 < http://www.unicode.org > 有字符图表、词汇表和 Unicode 规范的 PDF 版本。 为一些困难的阅读做好准备。 < http://www.unicode.org/history/ > 是 Unicode 起源和发展的年表。

为了帮助理解该标准,Jukka Korpela 编写了阅读 Unicode 字符表的介绍性指南,可在 < https://www.cs.tut.fi/~jkorpela/unicode/guide.html >.

乔尔·斯波尔斯基 (Joel Spolsky) 撰写的另一篇很好的介绍性文章 < http://www.joelonsoftware.com/articles/Unicode.html >. 如果这篇介绍没有让你清楚,你应该在继续之前尝试阅读这篇替代文章。

维基百科条目通常很有帮助; 请参阅“字符编码”条目< http://en.wikipedia.org/wiki/Character_encoding > 和 UTF-8 < http://en.wikipedia.org/wiki/UTF-8 > 例如。


Python 2.x 的 Unicode 支持

现在您已经了解了 Unicode 的基础知识,我们可以看看 Python 的 Unicode 特性。

Unicode 类型

Unicode 字符串表示为 unicode 类型的实例,这是 Python 的内置类型库之一。 它派生自一个名为 basestring 的抽象类型,它也是 str 类型的祖先; 因此,您可以使用 isinstance(value, basestring) 检查值是否为字符串类型。 在底层,Python 将 Unicode 字符串表示为 16 位或 32 位整数,具体取决于 Python 解释器的编译方式。

unicode() 构造函数具有签名 unicode(string[, encoding, errors])。 它的所有参数都应该是 8 位字符串。 第一个参数使用指定的编码转换为 Unicode; 如果您省略 encoding 参数,则将使用 ASCII 编码进行转换,因此大于 127 的字符将被视为错误:

>>> unicode('abcdef')
u'abcdef'
>>> s = unicode('abcdef')
>>> type(s)
<type 'unicode'>
>>> unicode('abcdef' + chr(255))    
Traceback (most recent call last):
...
UnicodeDecodeError: 'ascii' codec can't decode byte 0xff in position 6:
ordinal not in range(128)

errors 参数指定无法根据编码规则转换输入字符串时的响应。 此参数的合法值为“strict”(引发 UnicodeDecodeError 异常)、“replace”(添加 U+FFFD、“REPLACEMENT CHARACTER”)或“ignore”(仅将字符从 Unicode 结果中删除) )。 以下示例显示了差异:

>>> unicode('\x80abc', errors='strict')     
Traceback (most recent call last):
    ...
UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 0:
ordinal not in range(128)
>>> unicode('\x80abc', errors='replace')
u'\ufffdabc'
>>> unicode('\x80abc', errors='ignore')
u'abc'

编码被指定为包含编码名称的字符串。 Python 2.7 提供了大约 100 种不同的编码; 有关列表,请参阅 标准编码 中的 Python 库参考。 一些编码有多个名称; 例如,“latin-1”、“iso_8859_1”和“8859”都是相同编码的同义词。

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

>>> unichr(40960)
u'\ua000'
>>> ord(u'\ua000')
40960

unicode 类型的实例具有许多与 8 位字符串类型相同的方法,用于搜索和格式化等操作:

>>> s = u'Was ever feather so lightly blown to and fro as this multitude?'
>>> s.count('e')
5
>>> s.find('feather')
9
>>> s.find('bird')
-1
>>> s.replace('feather', 'sand')
u'Was ever sand so lightly blown to and fro as this multitude?'
>>> s.upper()
u'WAS EVER FEATHER SO LIGHTLY BLOWN TO AND FRO AS THIS MULTITUDE?'

请注意,这些方法的参数可以是 Unicode 字符串或 8 位字符串。 8 位字符串在执行操作前会被转换为 Unicode; 将使用 Python 的默认 ASCII 编码,因此大于 127 的字符将导致异常:

>>> s.find('Was\x9f')                   
Traceback (most recent call last):
    ...
UnicodeDecodeError: 'ascii' codec can't decode byte 0x9f in position 3:
ordinal not in range(128)
>>> s.find(u'Was\x9f')
-1

因此,许多对字符串进行操作的 Python 代码可以使用 Unicode 字符串,而无需对代码进行任何更改。 (输入和输出代码需要对 Unicode 进行更多更新;稍后会详细介绍。)

另一个重要的方法是 .encode([encoding], [errors='strict']),它返回 Unicode 字符串的 8 位字符串版本,以请求的编码进行编码。 errors 参数与 unicode() 构造函数的参数相同,增加了一种可能性; 除了 'strict'、'ignore' 和 'replace',您还可以传递使用 XML 字符引用的 'xmlcharrefreplace'。 以下示例显示了不同的结果:

>>> u = unichr(40960) + u'abcd' + unichr(1972)
>>> u.encode('utf-8')
'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')                       
Traceback (most recent call last):
    ...
UnicodeEncodeError: 'ascii' codec can't encode character u'\ua000' in
position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
'abcd'
>>> u.encode('ascii', 'replace')
'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
'&#40960;abcd&#1972;'

Python 的 8 位字符串有一个 .decode([encoding], [errors]) 方法,它使用给定的编码解释字符串:

>>> u = unichr(40960) + u'abcd' + unichr(1972)   # Assemble a string
>>> utf8_version = u.encode('utf-8')             # Encode as UTF-8
>>> type(utf8_version), utf8_version
(<type 'str'>, '\xea\x80\x80abcd\xde\xb4')
>>> u2 = utf8_version.decode('utf-8')            # Decode using UTF-8
>>> u == u2                                      # The two strings match
True

用于注册和访问可用编码的低级例程可在 codecs 模块中找到。 但是,这个模块返回的编解码功能通常比较底层而不舒服,所以我不打算在这里描述codecs模块。 如果您需要实现一种全新的编码,则需要了解 codecs 模块接口,但实现编码是一项专门的任务,这里也不会介绍。 请查阅 Python 文档以了解有关此模块的更多信息。

codecs 模块最常用的部分是 codecs.open() 函数,将在输入和输出部分讨论。


Python 源代码中的 Unicode 文字

在 Python 源代码中,Unicode 文字被写成以“u”或“U”字符为前缀的字符串:u'abcdefghijk'。 可以使用 \u 转义序列编写特定的代码点,该序列后跟四个给出代码点的十六进制数字。 \U 转义序列类似,但需要 8 个十六进制数字,而不是 4 个。

Unicode 文字也可以使用与 8 位字符串相同的转义序列,包括 \x,但 \x 只需要两个十六进制数字,因此它不能表示任意代码点。 八进制转义可以达到 U+01ff,即八进制 777。

>>> s = u"a\xac\u1234\u20ac\U00008000"
... #      ^^^^ two-digit hex escape
... #          ^^^^^^ four-digit Unicode escape
... #                      ^^^^^^^^^^ eight-digit Unicode escape
>>> for c in s:  print ord(c),
...
97 172 4660 8364 32768

对大于 127 的代码点使用转义序列在小剂量下是没问题的,但是如果您使用许多重音字符,就像在包含法语或其他一些使用重音的语言的消息的程序中一样,这会变得很烦人。 您还可以使用 unichr() 内置函数组合字符串,但这更加乏味。

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

Python 支持以任何编码编写 Unicode 文字,但您必须声明正在使用的编码。 这是通过在源文件的第一行或第二行包含一个特殊注释来完成的:

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

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

该语法的灵感来自 Emacs 用于指定文件局部变量的符号。 Emacs 支持许多不同的变量,但 Python 仅支持“编码”。 -*- 符号向 Emacs 表明该注释是特殊的; 它们对 Python 没有意义,而是一种约定。 Python 在注释中查找 coding: namecoding=name

如果不包含这样的注释,则使用的默认编码将是 ASCII。 2.4 之前的 Python 版本以欧洲为中心,并假定 Latin-1 作为字符串文字的默认编码; 在 Python 2.4 中,大于 127 的字符仍然有效,但会导致警告。 例如,以下程序没有编码声明:

#!/usr/bin/env python
u = u'abcdé'
print ord(u[-1])

当你用 Python 2.4 运行它时,它会输出以下警告:

amk:~$ python2.4 p263.py
sys:1: DeprecationWarning: Non-ASCII character '\xe9'
     in file p263.py on line 2, but no encoding declared;
     see https://www.python.org/peps/pep-0263.html for details

Python 2.5 及更高版本更严格,会产生语法错误:

amk:~$ python2.5 p263.py
File "/tmp/p263.py", line 2
SyntaxError: Non-ASCII character '\xc3' in file /tmp/p263.py
  on line 2, but no encoding declared; see
  https://www.python.org/peps/pep-0263.html for details

Unicode 属性

Unicode 规范包括一个有关代码点的信息数据库。 对于定义的每个代码点,信息包括字符的名称、类别、数值(如果适用)(Unicode 具有表示罗马数字和分数的字符,例如三分之一和五分之四)。 还有一些与代码点在双向文本中的使用相关的属性和其他与显示相关的属性。

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

import unicodedata

u = unichr(233) + unichr(0x0bf2) + unichr(3972) + unichr(6000) + unichr(13231)

for i, c in enumerate(u):
    print i, '%04x' % ord(c), unicodedata.category(c),
    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' 表示“标记,非空格”,[ X148X] 是“符号,其他”。 见 < http://www.unicode.org/reports/tr44/#General_Category_Values > 获取类别代码列表。


参考

Unicode 和 8 位字符串类型在 Python 库参考 序列类型 - str、unicode、列表、元组、字节数组、缓冲区、xrange 中进行了描述。

unicodedata 模块的文档。

codecs 模块的文档。

Marc-André Lemburg 在 EuroPython 2002 上发表了题为“Python 和 Unicode”的演讲。 他的幻灯片的 PDF 版本可在 < https://downloads.egenix.com/python/Unicode-EPC2002-Talk.pdf >,是对 Python Unicode 特性设计的出色概述。


读取和写入 Unicode 数据

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

根据您的输入源和输出目的地,您可能不需要做任何事情; 您应该检查应用程序中使用的库是否原生支持 Unicode。 例如,XML 解析器经常返回 Unicode 数据。 许多关系数据库还支持 Unicode 值列,并且可以从 SQL 查询返回 Unicode 值。

Unicode 数据在写入磁盘或通过套接字发送之前通常会转换为特定的编码。 可以自己完成所有工作:打开一个文件,从中读取一个 8 位字符串,然后使用 unicode(str, encoding) 转换该字符串。 但是,不推荐手动方法。

一个问题是编码的多字节性质; 一个 Unicode 字符可以用几个字节表示。 如果您想以任意大小的块(例如 1K 或 4K)读取文件,则需要编写错误处理代码来捕获仅读取编码单个 Unicode 字符的部分字节的情况。块。 一种解决方案是将整个文件读入内存,然后执行解码,但这会阻止您处理非常大的文件; 如果您需要读取 2Gb 的文件,则需要 2Gb 的 RAM。 (更多,真的,因为至少有一段时间你需要在内存中同时拥有编码字符串和它的 Unicode 版本。)

解决方案是使用低级解码接口来捕获部分编码序列的情况。 实现这一点的工作已经为您完成:codecs 模块包含一个版本的 open() 函数,该函数返回一个类文件对象,假设文件的内容在指定的编码并接受 .read().write() 等方法的 Unicode 参数。

该函数的参数是open(filename, mode='rb', encoding=None, errors='strict', buffering=1)mode 可以是 'r''w''a',就像普通内置 open() 函数的对应参数一样; 添加 '+' 以更新文件。 buffering 与标准函数的参数类似。 encoding 是一个字符串,给出要使用的编码; 如果保留为 None,则返回一个接受 8 位字符串的常规 Python 文件对象。 否则,将返回一个包装对象,并且将根据需要转换写入或从包装对象读取的数据。 errors 指定编码错误的操作,可以是“strict”、“ignore”和“replace”的常用值之一。

因此,从文件中读取 Unicode 很简单:

import codecs
f = codecs.open('unicode.rst', encoding='utf-8')
for line in f:
    print repr(line)

也可以在更新模式下打开文件,允许读取和写入:

f = codecs.open('test', encoding='utf-8', mode='w+')
f.write(u'\u4500 blah blah blah\n')
f.seek(0)
print repr(f.readline()[:1])
f.close()

Unicode 字符 U+FEFF 用作字节顺序标记 (BOM),通常作为文件的第一个字符写入,以帮助自动检测文件的字节顺序。 某些编码(例如 UTF-16)期望在文件开头出现 BOM; 当使用这种编码时,BOM 将自动作为第一个字符写入,并在读取文件时静默删除。 这些编码有多种变体,例如用于 little-endian 和 big-endian 编码的 'utf-16-le' 和 'utf-16-be',它们指定一种特定的字节顺序并且不跳过 BOM。

Unicode 文件名

当今常用的大多数操作系统都支持包含任意 Unicode 字符的文件名。 通常这是通过将 Unicode 字符串转换为某种因系统而异的编码来实现的。 例如,Mac OS X 使用 UTF-8,而 Windows 使用可配置的编码; 在 Windows 上,Python 使用名称“mbcs”来指代当前配置的任何编码。 在 Unix 系统上,只有设置了 LANGLC_CTYPE 环境变量,才会有文件系统编码; 如果没有,默认编码是 ASCII。

sys.getfilesystemencoding() 函数返回要在当前系统上使用的编码,以防您想手动进行编码,但没有太多理由费心。 打开文件进行读取或写入时,通常只需提供 Unicode 字符串作为文件名,它就会自动转换为正确的编码:

filename = u'filename\u4500abc'
f = open(filename, 'w')
f.write('blah\n')
f.close()

os 模块中的函数,例如 os.stat() 也将接受 Unicode 文件名。

os.listdir() 返回文件名,提出了一个问题:它应该返回文件名的 Unicode 版本,还是应该返回包含编码版本的 8 位字符串? os.listdir() 两者都可以,具体取决于您提供的目录路径是 8 位字符串还是 Unicode 字符串。 如果您将 Unicode 字符串作为路径传递,文件名将使用文件系统的编码进行解码并返回 Unicode 字符串列表,而传递 8 位路径将返回文件名的 8 位版本。 例如,假设默认文件系统编码为 UTF-8,运行以下程序:

fn = u'filename\u4500abc'
f = open(fn, 'w')
f.close()

import os
print os.listdir('.')
print os.listdir(u'.')

将产生以下输出:

amk:~$ python t.py
['.svn', 'filename\xe4\x94\x80abc', ...]
[u'.svn', u'filename\u4500abc', ...]

第一个列表包含 UTF-8 编码的文件名,第二个列表包含 Unicode 版本。


编写可识别 Unicode 的程序的技巧

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

最重要的提示是:

软件应该只在内部使用 Unicode 字符串,在输出时转换为特定的编码。


如果您尝试编写同时接受 Unicode 和 8 位字符串的处理函数,您会发现您的程序在组合这两种不同类型的字符串的任何地方都容易出现错误。 Python 的默认编码是 ASCII,因此每当输入数据中出现 ASCII 值 > 127 的字符时,您将得到 UnicodeDecodeError,因为该字符无法由 ASCII 编码处理。

如果您仅使用不包含任何重音的数据来测试您的软件,则很容易错过此类问题; 一切似乎都可以正常工作,但您的程序中实际上存在一个错误,等待第一个尝试使用字符 > 127 的用户。 因此,第二个技巧是:

在测试数据中包含 > 127 个字符,甚至 > 255 个字符更好。


当使用来自 Web 浏览器或其他不受信任来源的数据时,常用的技术是在生成的命令行中使用字符串或将其存储在数据库中之前检查字符串中的非法字符。 如果您这样做,请小心检查字符串,一旦它处于将要使用或存储的形式; 可以使用编码来伪装字符。 如果输入数据还指定了编码,则尤其如此; 许多编码不考虑通常检查的字符,但 Python 包含一些编码,例如 'base64' 修改每个单个字符。

例如,假设您有一个采用 Unicode 文件名的内容管理系统,并且您想禁止带有“/”字符的路径。 您可能会编写以下代码:

def read_file (filename, encoding):
    if '/' in filename:
        raise ValueError("'/' not allowed in filenames")
    unicode_name = filename.decode(encoding)
    f = open(unicode_name, 'r')
    # ... return contents of file ...

但是,如果攻击者可以指定 'base64' 编码,他们可以通过 'L2V0Yy9wYXNzd2Q='(字符串 '/etc/passwd' 的 base-64 编码形式)来读取系统文件。 上面的代码在编码形式中查找 '/' 字符,但在结果解码形式中遗漏了危险字符。


参考

Marc-André Lemburg 的演讲“Writing Unicode-aware Applications in Python”的 PDF 幻灯片可在 < https://downloads.egenix.com/python/LSM2005-Developing-Unicode-aware-applications-in-Python.pdf > 并讨论字符编码问题以及如何国际化和本地化应用程序。


修订历史和致谢

感谢以下指出本文错误或提出建议的人:Nicholas Bastin、Marius Gedminas、Kent Johnson、Ken Krugler、Marc-André Lemburg、Martin von Löwis、Chad Whitacre。

1.0 版:2005 年 8 月 5 日发布。

1.01 版:2005 年 8 月 7 日发布。 纠正事实和标记错误; 添加几个链接。

1.02 版:2005 年 8 月 16 日发布。 纠正事实错误。

1.03 版:2010 年 6 月 20 日发布。 请注意,Python 3.x 未涵盖,并且 HOWTO 仅涵盖 2.x。