Unicode HOWTO

  • Release

    • 1.03

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

Unicode 简介

字符代码的历史记录

1968 年,美国信息交换标准代码(以 ASCII 首字母缩写而闻名)被标准化。 ASCII 定义的用于各种字符的数字代码,其数字值从 0 到 127.例如,小写字母'a'被指定为其代码值 97.

ASCII 是美国开发的标准,因此它仅定义了不带重音的字符。有一个“ e”,但没有一个“é”或“Í”。这意味着需要重音符号的语言无法如实地用 ASCII 表示。 (实际上,丢失的重音对英语也很重要,其中包含“天真”和“咖啡厅”之类的单词,有些出版物的房屋风格要求拼写为“coöperate”.)

有一阵子,人们只是写了没有口音的程序。我记得我看过 1980 年代中期在法语出版物中发表的 Apple] [BASIC 程序],其内容如下:

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

这些邮件应包含重音,对于那些会读法语的人来说,它们看起来是错误的。

在 1980 年代,几乎所有个人计算机都是 8 位的,这意味着字节可以容纳 0 到 255 之间的值。ASCII 码最多只能达到 127,因此某些机器将 128 到 255 之间的值分配给重音字符。但是,不同的机器具有不同的代码,这导致交换文件时出现问题。finally出现了 128-255 范围内的各种常用值集。一些是由国际标准化组织定义的真实标准,而另一些是由一家公司或另一家公司发明并成功流行的“事实上的”公约。

255 个字符不是很多。例如,您不能同时将西欧使用的重音字符和俄语使用的西里尔字母都放入 128-255 范围内,因为这样的字符超过 128 个。

您可以使用不同的代码编写文件(在名为 KOI8 的编码系统中所有的俄语文件,在名为 Latin1 的不同编码系统中的所有法语文件),但是如果您想编写引用一些俄语文本的法语文件怎么办?在 1980 年代,人们开始想解决这个问题,并且 Unicode 标准化工作开始了。

Unicode 最初使用 16 位字符而不是 8 位字符。 16 位表示您拥有 2 ^ 16 = 65,536 个不同的值,从而可以表示来自许多不同字母的许多不同字符;最初的目标是使 Unicode 包含每种人类语言的字母。事实证明,即使 16 位也不足以满足该目标,现代 Unicode 规范使用的编码范围更广,范围为 0–1,114,111(base-16 为 0x10ffff)。

有一个相关的 ISO 标准,即 ISO10646.Unicode 和 ISO 10646 最初是单独的工作,但是这些规范已与 Unicode 的 1.1 修订版合并。

(对 Unicode 历史的讨论已大大简化.我认为一般的 Python 程序员不必担心历史细节;有关更多信息,请参阅参考资料中列出的 Unicode 联合站点.)

Definitions

“字符”是文本的最小可能组成部分。 'A','B','C'等都是不同的字符。 “È”和“Í”也是如此。字符是抽象的,并且根据您所谈论的语言或上下文而有所不同。例如,欧姆(Ω)的符号通常很像希腊字母中的大写字母 omega(Ω)(在某些字体中它们甚至可能相同)绘制,但是这是两个具有不同含义的不同字符。

Unicode 标准描述了如何pass 代码点 来表示字符。代码点是一个整数值,通常以 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 工具包或终端的字体渲染器的工作。

Encodings

总结上一节:Unicode 字符串是一系列代码点,它们是从 0 到 0x10ffff 的数字。此序列需要表示为内存中的一组字节(即 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

这种表示很简单,但是使用它会带来很多问题。

  • 它不是便携式的;不同的处理器对字节的排序不同。

  • 这是非常浪费的空间。在大多数文本中,大多数代码点小于 127,或小于 255,因此零字节占用了大量空间。上面的字符串占用 24 个字节,而 ASCII 表示形式则需要 6 个字节。增加的 RAM 使用量并不太重要(台式计算机具有 MB 的 RAM,并且字符串通常不会那么大),但是将磁盘和网络带宽的使用扩展 4 倍是无法忍受的。

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

  • 许多 Internet 标准是根据文本数据定义的,无法处理嵌入了零字节的内容。

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

编码不必处理所有可能的 Unicode 字符,大多数编码不需要。例如,Python 的默认编码是'ascii'编码。将 Unicode 字符串转换为 ASCII 编码的规则很简单。对于每个代码点:

  • 如果代码点<128,则每个字节与代码点的值相同。

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

Latin-1,也称为 ISO-8859-1,是类似的编码。 Unicode 代码点 0–255 与 Latin-1 值相同,因此转换为这种编码只需要将代码点转换为字节值即可。如果遇到大于 255 的代码点,则无法将字符串编码为 Latin-1.

编码不必像 Latin-1 这样简单的Pair一 Map。考虑一下 IBM 大型机上使用的 IBM EBCDIC。字母值不在一个块中:“ a”到“ i”的值从 129 到 137,但是“ j”到“ r”的值从 145 到 153.如果您想使用 EBCDIC 作为编码,则可能使用某种查找表执行转换,但这在很大程度上是内部细节。

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

  • 如果代码点<128,则由相应的字节值表示。

  • 如果代码点在 128 和 0x7ff 之间,则它将变成两个介于 128 和 255 之间的字节值。

0x7ff 的代码点被转换为三字节或四字节序列,其中该序列的每个字节在 128 和 255 之间。

UTF-8 具有几个方便的属性:

  • 它可以处理任何 Unicode 代码点。

  • Unicode 字符串转换为不包含嵌入式零字节的字节字符串。这避免了字节排序问题,并且意味着 UTF-8 字符串可以由strcpy()之类的 C 函数处理,并pass不能处理零字节的协议发送。

  • ASCII 文本字符串也是有效的 UTF-8 文本。

  • UTF-8 非常紧凑;大多数代码点都变成了两个字节,小于 128 的值仅占用一个字节。

  • 如果字节损坏或丢失,则可以确定下一个 UTF-8 编码的代码点的开始并重新同步。随机的 8 位数据也不太可能看起来像有效的 UTF-8.

References

< http://www.unicode.org >的 Unicode 联合会站点提供字符表,词汇表和 Unicode 规范的 PDF 版本。为阅读困难做好准备。 < http://www.unicode.org/history/ >是 Unicode 起源和 Developing 的年代表。

为了帮助理解该标准,Jukka Korpela 编写了入门指南,以阅读< https://www.cs.tut.fi/~jkorpela/unicode/guide.html >提供的 Unicode 字符表。

Joel Spolsky < http://www.joelonsoftware.com/articles/Unicode.html >撰写了另一篇很好的入门文章。如果此简介不能使您理解清楚,则应在 continue 之前try阅读本替代文章。

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

Python 2.x 的 Unicode 支持

既然您已经了解了 Unicode 的基础知识,我们就可以看看 Python 的 Unicode Function。

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参数指定当无法根据编码规则转换 Importing 字符串时的响应。此参数的合法值为'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 种不同的编码。有关列表,请参见Standard Encodings处的 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 字符串一起使用,而无需对代码进行任何更改。 (Importing 和输出代码需要对 Unicode 进行更多更新;稍后将对此进行更多说明.)

另一个重要的方法是.encode([encoding], [errors='strict']),它返回以请求的 encodings 编码的 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模块中提供了用于注册和访问可用编码的低级例程。但是,此模块返回的编码和解码Function通常比舒适度更底层,因此在这里我将不介绍codecs模块。如果需要实现全新的编码,则需要了解codecs模块接口,但是实现编码是一项专门的任务,此处也将不介绍。请查阅 Python 文档以了解有关此模块的更多信息。

codecs模块最常用的部分是codecs.open()函数,有关 Importing 和输出的部分将对此进行讨论。

Python 源代码中的 UnicodeLiterals

在 Python 源代码中,UnicodeLiterals 被编写为以'u'或'U'字符为首的字符串u'abcdefghijk'。可以使用\u转义序列编写特定的代码点,其后是四个十六进制数字,以表示代码点。 \U的转义序列相似,但是期望 8 个十六进制数字,而不是 4.

UnicodeLiterals 也可以使用与 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()内置函数来汇编字符串,但这更加乏味。

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

Python 支持以任何编码形式编写 UnicodeLiterals,但是您必须语句所使用的编码。这可以pass在源文件的第一行或第二行中添加特殊 Comments 来完成:

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

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

该语法受 Emacs 用于指定文件本地变量的符号的启发。 Emacs 支持许多不同的变量,但是 Python 仅支持“编码”。 -*-符号向 Emacs 表示 Comments 是特殊的;它们对 Python 没有意义,只是一个约定。 Python 在 Comments 中查找coding: namecoding=name

如果您不包含此类 Comments,则使用的默认编码为 ASCII。 2.4 之前的 Python 版本以欧洲为中心,并假定 Latin-1 为字符串 Literals 的默认编码;在 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 Properties

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'表示“ Number,other”,'Mn'是“ Mark,nonspacing”,'So'是“ Symbol,other”。有关类别代码的列表,请参见< http://www.unicode.org/reports/tr44/#General_Category_Values >。

References

Unicode 和 8 位字符串类型在序列类型— str,unicode,list,tuple,bytearray,buffer,xrange的 Python 库参考中描述。

unicodedata模块的文档。

codecs模块的文档。

Marc-AndréLemburg 在 EuroPython 2002 上作了题为“ Python 和 Unicode”的演讲。可以在< https://downloads.egenix.com/python/Unicode-EPC2002-Talk.pdf >获得他的幻灯片的 PDF 版本,并且很好地概述了 Python 的 Unicode Function的设计。

读写 Unicode 数据

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

根据 Importing 源和输出目的地,可能不需要执行任何操作。您应该检查应用程序中使用的库是否本机支持 Unicode。例如,XML 解析器通常返回 Unicode 数据。许多关系数据库还支持 Unicode 值列,并且可以从 SQL 查询返回 Unicode 值。

Unicode 数据通常在写入磁盘或pass套接字发送之前先转换为特定的编码。可以自己完成所有工作:打开文件,从文件中读取 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 用作字节 Sequences 标记(BOM),通常被写为文件的第一个字符,以帮助自动检测文件的字节 Sequences。某些编码(例如 UTF-16)期望 BOM 表出现在文件的开头;当使用这种编码时,BOM 将自动作为第一个字符写入,并且在读取文件时将被静默删除。这些编码有多种变体,例如用于 Little-endian 和 Big-endian 编码的'utf-16-le'和'utf-16-be',它们指定一个特定的字节 Sequences,并且不会跳过 BOM。

Unicode filenames

今天,大多数常用的 os 都支持包含任意 Unicode 字符的文件名。通常,这是pass将 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 的软件的一些建议。

最重要的提示是:

Note

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

如果try编写同时接受 Unicode 和 8 位字符串的处理函数,则无论将两种不同类型的字符串组合在一起,都会发现程序容易受到错误的影响。 Python 的默认编码为 ASCII,因此只要 Importing 数据中的 ASCII 值> 127 的字符都为UnicodeDecodeError,因为 ASCII 编码无法处理该字符。

如果仅使用不包含任何重点的数据来测试软件,就很容易错过此类问题。一切似乎都可以正常运行,但是程序中实际上存在一个错误,await 第一个try使用字符> 127 的用户。第二个技巧是:

Note

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

当使用来自 Web 浏览器或其他不受信任来源的数据时,一种常见的技术是在生成的命令行中使用字符串或将其存储在数据库中之前检查字符串中的非法字符。如果要执行此操作,则在将字符串以将要使用或存储的形式存储时,请务必检查该字符串。有可能使用编码来伪装字符。如果 Importing 数据还指定了编码,则尤其如此。许多编码都保留了通常检查的字符,但是 Python 包含一些诸如'base64'之类的编码,它们会修改每个字符。

例如,假设您有一个采用 Unicode 文件名的内容 Management 系统,并且要禁止使用'/'字符的路径。您可以编写以下代码:

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 编码形式)来读取系统文件。上面的代码以编码形式查找'/'个字符,并以finally的解码形式丢失危险字符。

References

马克安德烈·莱姆堡(Marc-AndréLemburg)的演讲“用 Python 编写支持 Unicode 的应用程序”的 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 vonLö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。