关闭

Python Unicode 基本知识

标签: Unicode.Python编码utf-8
1157人阅读 评论(0) 收藏 举报
分类:

Python Unicode 基本知识


本文讨论了 Python 2.x 对 Unicode 的支持,并对人们在使用 Unicode 时常遇到的问题进行了解释。


Unicode 简介


1. 字符编码历史回顾



1968 年,美国信息交换标准码(众所周知的 ASCII 码)被标准化。 ASCII 为各种字符定义了对应的数字代码,这些数字代码的取值范围为 0 - 127.  例如, 小写字母 ‘a' 被分配的码值为 97.

ASCII 是美国开发出的标准,因此该标准中只定义了那些不带重音的字符。例如,ASCII 中定义了 ’e', 但是却没有 ‘é’ 和 ‘Í’ 这种重音字符。这意味着带重音的语言,无法用 ASCII 忠实无误的展现出来 (其实,不带重音对英语本身也有影响,例如,英文的单词‘naïve’ and ‘café’,以及一些出版物需要使用 ‘coöperate’ 这样的印刷格式,都是 ASCII 无法处的)。

因此有段时期,人们只写那些不显示重音的程序。我记得 1980 年代中期,法语出版物中刊出的 Apple ][ BASIC 程序像下面这样:

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

这些打印的信息中本应该包含重音,对懂得法语的人来说,他们显然错了。
在 1980 年代,几乎所有的个人电脑都是 8 位的,意思是一个字节可以存储的值的范围是 0 - 255. ASCII 中仅使用了 0 - 127, 因此有些机器将 128 - 255 之间的值分配给重音字符。不同的机器有不同的分配方案,这会导致文件交换时出问题。最终,各种常用的取值在 128 -255 之间的字符集出现了。其中的一些由 ISO 定制成为了标准,另一些由一些公司发明和管理也成了事实上的约定。

255 个字符并不多,例如你无法将西欧使用的重音字符和俄语使用的斯拉夫字母同时放在 128 -255 这段范围,因为这些字符的总数已经超过了128。

你可以使用不同的编码来创建不同的文件(所有俄文文件使用一种编码系统 K0I8,所有法语文件使用另一种编码系统 Latin1),但是如果你想谢一个法语文件,但是里面你想引用一段俄语,这该怎么办呢?在 1980 年代,人们试图解决这样的问题,Unicode 标准化的工作随之开始。

Unicode 刚开始使用 16 位(bit)字符,16 位意味着你有 2 ^ 16 = 65536 个不中的值,使得表示来自不同字符集的更多的不同的字符成为可能。最初的目标是让 Unicode 包含所有人类语言的字符集。结果发现即使 16 位字符也无法满足要求,而现代的 Unicode 使用的码值范围为:0 - 1114111 (0x10ffff 十六进制)。

关于 Unicode,有一个相关的 ISO标准: ISO 10646。Unicode项目 和 ISO 10646 最初是各自开发的,但是在 Unicode 1.1 修订版中,这两者进行了合并。

(这段关于 Unicode 历史的讨论机器简单。普通的 Python 程序员不需要关心这些历史细节,并且 Unicode 委员会的网站上给出了更多相关的信息)


2.定义


字符是文本的最小组成单位。‘A', 'B', 'C‘ 这些都是不同的字符。同样的,‘È’ 和 ‘Í’ 也是如此。字符是个抽象的概念,非常依赖于所用的语言及谈话的上下文。例如,符号欧姆(Ω)常常被写得像大写的希腊字母欧米噶(Ω)(在一些字体中,这两者的写法甚至完全一样),但是他们是不同的字符,具有不同的意义。

Unicode 标准描述了字符是如何使用代码点(code points)来表示的。一个代码点就是一个整形值,通常用 16 进制表示。在标准中,代码点使用 U+12ca (十进制 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 是个代码点,它代表了某个特定的字符;在 Unicode 中,它代表的是 ‘ETHIOPIC SYLLABLE WI’。在非正式的上下文中,代码点和字符之间的区别经常被忽略。

在屏幕或纸质文档上,字符通常用几何形状来表示,这就是所谓的图形字符(glyph)。例如大写字母 A 的图形字符由两个斜线和一条水平线构成,尽管更细节的构成还依赖于使用的字体。绝大多数 Python 代码不需要关系字符的图形表示。确定字符对应的正确的图形表示是 GUI 程序和终端字体渲染要做的工作。


3. 编码


作为对以上内容的总结:一个 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
这种编码方法很直接,但是存在一些问题:

a. 不可移植,因为不同的处理器采用不同的字节序

b. 非常浪费空间。 在大多数文本中,主要的代码点的值都小于 127,或小于 255, 因此在这种编码方法中有非常多的空间被 0 字节占据。上面的字符串需要 24 个字节,而 ASCII 编码却仅需要 6 字节。RAM 使用量的增大还不太要紧,但是磁盘和网络带宽的使用量会 4 倍的增大,这是难以容忍的。

c. 和既有的 C 函数,如 strlen(),不兼容,因此,需要使用一组新的宽字符函数族

d. 许多因特网标准是基于文本数据定义的,不能处理嵌入了 0 字节的内容。


通常人们不会选用这样的编码,而是选择那些更加高效和方便的编码方式。UTF-8是一种被广泛支持的编码方式,下面会进行讨论:

编码不必处理每一个可能的 Unicode 字符,并且大部分编码都是如此。例如,Python 默认的编码方式是 ‘ascii’。将 Unicode 字符串装换成 ASCII 编码非常简单,对于每个代码点:

a. 如果代码点小于 128, 每个字节的值和代码点的值相同

b. 如果代码点值大于等于 128,这类 Unicode 字符不能用 ASCII 编码表示, 这种情况下,Python 会抛出 UnicodeEncodeError 异常


Latin-1,又称为 ISO-8859-1,是一种相似的编码方式。Unicode 代码点值在 0 - 255 范围内的 Unicode 字符的码值和 Latin-1编码方式下字节的值相同,因此将 Unicode 进行 Latin-1 编码,只需简单的把 Unicode 字符的码值转换成对应的字节值;如果遇到值大于 255 的代码点,这样的字符就不能用 Latin-1 编码。编码不必采用 Latin-1 这种简单的按顺序的一一对应的映射方式,例如使用在 IBM 大型机上的 EBCDIC 编码。字母 a 到 i 的值从129 到 137,但是字母 j 到 r 的值却从145 到 153。


UTF-8 是最广泛使用的编码方式之一。UTF表示“统一码转换格式”(Unicode Transformation Format), 8 的意思是编码中使用 8 位 (8 bit)数字。(类似的还有 UTF-16等编码方式,但是不如 UTF-8 常用), UTF-8 编码规则如下:

a. 如果代码点小于 128,用相应的字节值表示该 Unicode 字符

b. 如果代码点在 128 到 0x7ff之间,这类代码点被转换成字节值在 128 - 255 之间的两个字节值。

c. 代码点大于 0x7ff,这类 代码点对应的 Unicode 将使用 3 - 4 个字节序列表示, 序列中每个字节的值都在 128 - 255 范围内。

详细的编码规则请参考:http://blog.csdn.net/qq_26886929/article/details/53993823


UTF-8 编码具有一些方便的特性:

a. 可以处理任何 Unicode 代码点

b. Unicode 字符串被编码后得到的是没有嵌入 0 字节的字节串(字节序列)。这避免的字节序的问题,也意味着 UTF-8编码后的字节串可以使用 strcpy() 这样的 C 函数处理,也可以使用那些不能处理 0 字节的协议发送。

c. ASCII 字符组成的字符串也是一个有效的 UTF-8 文本。

d. UTF-8 相当紧凑,编码后大部分代码点被转换成 2 字节, 码值小于 128的仅需要 1 个字节。

e. 如果某些字节损坏或丢失,判断下一个 UTF-8 编码的代码点的开始并重新同步是可能的。并且随机的 8 位数据串看起来像有效的 UTF-8 也是不太可能的。


3. 参考文献


Unicode 协会网站:http://www.unicode.org,可以找到字符表,及PDF格式的 Unicode 说明。在 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, 如果本文没有帮你把 Unicode相关知识弄清楚,你应该先读一读这篇文章。
维基百科通常比较有用,请看其中的如下词条:“character encoding” : http://en.wikipedia.org/wiki/Character_encoding ; “UTF-8” : http://en.wikipedia.org/wiki/UTF-8


4. Python 2.x 对 Unicode 的支持


我们已经学习了 Unicode 的基础知识,现在来看看 Python 对Unicode 的支持。

4.1 Unicode 类型

Unicode 字符串被表达为 unicode 类型的实例,unicode 是 Python的内置类型之一。unicode 类型由抽象类 basestring 派生而来,同时 basestring 类还是 str 类的父类;因此你可以通过下面的表达式来检查某个值是否是字符串类型: isinstance(value, basestring)。在底层,根据编译的差异,Python 解释器把 Unicode字符串表示为 16或32位整数。

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)

构造函数中的 error 参数指定了当输入的字节串无法用给定的编码方式进行转换时,函数的响应方式。该参数的有效值是: ‘strict’ (抛出 UnicodeDecodeError异常);‘replace’ (替换成代码点为 U+FFFD的替换字符‘REPLACEMENT CHARACTER’); 'ignore' (在解码结果中去掉无法解码的字节)。下面是具体的例子:

>>> 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 支持大约 100 编码方式,有些编码方式有几种不同的名称,例如:'latin-1', 'iso_8859_1', '8859' 都是指同一种编码方式。

单字节的 Unicode 字符串也可以使用内置函数 unichr() 来创建。该函数接受一个整形数作为参数,返回一个长度为 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 字符串也同样工作。


另一个重要的方法是 .encode([encoding], [errors='strict']),该函数返回使用指定编码后得到的参数中的 Unicode 字符串所对应的字节串。errors 参数的意义与 unicode() 里面的一致,并且这里还可以接受另外的一个值:‘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]) 方法,该方法是用指定的编码方式来解码字节串,得到其对应的 Unicode 字符串:

>>> 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 模块最常使用的部分是 codecs.open() 函数,下面会有讨论。


4.2 Python 源代码中的 Unicode 字符串字面值

在 Python 源代码中 Unicode 字面值被写成以 ‘u’ 或 ‘U’开头的字符串,如:u'abcdefghijk'。特殊的代码点可以写成 \u 转义序列,\u 后面跟着对应的十六进制代码点。\U转义序列比较类似,但是它后面跟着的是 8 个 16进制数字,而不是 4 个。

和 8 位字节串一样,Unicode 字面值中也可以使用 \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 的 Unicode 字符,少量使用转义序列式可以的,但是如果你在使用一门有许多重音字符的语言,例如法语,这样的转义序列就会让人厌烦。比较理想的方式是,用你所使用语言自身的编码来写字面值。这样你就可以使用你自喜欢的编辑器来编写 Python 原文件,并且那些重音字符可以很自然的展现眼前,在运行时使用的也是对的字符。


Python 支持使用任意的编码来写 Unicode 字面值,但是你必须指定使用的编码。这是通过在源文件的第一或第二行添加下面的注释来实现的:

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

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

这种是 Emacs 编辑器推荐的语法。Emacs 支持许多不同的变量,但是 Python 只支持 'coding'。-*- 这个符号,告诉 Emacs 这是个特殊的注释,但是对 Python 并不重要。 Python 只会在注释里找 coding: name 或 coding=name 这样的记号。

如果你不给定这样的注释,默认使用的编码将会是 ASCII。Python 2.4 以前的版本是以欧洲为中心的,并且假设字符串字面值默认的编码为 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

4.3 Unicode 特性

Unicode 说明中包含了一个代码点信息的数据库。对每个定义的代码点,信息中包括:字符名称,字符类别,数字值(如果有的话,Unicode 中有字符代表罗马数字和分数,例如1/3,4/5等)。下面的程序显示了一些字符的相关信息,并打印出了其中一个字符的数字值:

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‘ 表示, ’标记,非空格‘,’So‘ 表示,’符号,其他‘,可以访问 http://www.unicode.org/reports/tr44/#General_Category_Values 查看更多类别代码。


5.读写 Unicode 数据


一旦你写了一些和 Unicode 一起工作的代码,接下来的问题就是输入和输出。 你怎样将 Unicode 字符串输入到你的程序呢,你怎样将 Unicode 转换成一种适合存储或者传输的格式呢?

根据你的输入源和输出目的点,你可能不需要做任何修改,你应该检查你应用程序中使用的库是否原生支持 Unicode。XML 解析器通常返回 Unicode  数据,许多关系数据库也支持使用 Unicode 作为列值,并且使用 SQL 查询可以返回 Unicode 值。

Unicode 数据在写磁盘或者通过网络发送时,通常要进行适当的编码。你可以自己完成这些工作:打开文件,从中读取字节串,将字节串转换成 Unicode,然而这种方式并不推荐。

其他一个问题是编码的多字节本性;一个 Unicode 字符可能需要多个字节来表示。如果你想以任意的块大小(如 1K,4K)来读取文件,你需要写异常处理代码来处理这种情况:文件块读完后,某个 Unicode 字母对应的字节码中只读入了部分字节。有个解决方法是将文件整个读入内存,然后进行解码,但是这样限制了你对超大文件的处理。如果你读取一个 2 GB的文件,你将需要大于 2 GB的 RAM。

这个问题的解决方法是使用底层的解码接口来处理这种情况。这些早已经实现了:在 codecs 模块,包含一个 open() 方法,它返回一个类文件的对象,并且假定文件内容是用指定的方式编码的,并且该对象的 .read() 和 .write() 方法可以接受 Unicode 参数。

函数参数如下: open(filename, mode='rb', encoding=None, errors='strict', buffering=1)。mode的取值可以为 ‘r’, 'w', 'a' 就行普通的打开文件的 open 函数的参数一样。encoding 参数用于指定使用的编码方式,如果为None, 函数就会返回一个普通的 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 被用作字节序标志(byte-order mark, BOM),通常被写作文件的第一个字符以使得文件字节序能够自动的检查出来。一些编码,例如 UTF-16,希望文件开头存在一个 BOM,当这种编码被使用时,BOM会被自动写作文件的首个字符,并且在读取文件时将其丢弃。


6. Unicode 文件名

现今使用的绝大多数操作系统自持在文件名中使用任意的 Unicode 字符。通常这是通过将 Unicode 转换成某种编码来实现的,具体编码与操作系统相关。例如 Mac OS X 使用 UTF-8 而 Windows 则使用可配置的编码;在 Windows 中,Python 使用 ‘mbcs’来指代当前使用的编码方式。在 Unix 系统中,如果你设置了 LANG 或者 LC_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 字符串形式呢,还是返回编码后的字节串格式呢?os.listdir() 两种都可以返回,根据你提供的路径是 Unicode 字符串还是字节串进行选择。如果你传递 Unicode 字符串作为路径名,文件名就会使用系统编码方式进行解码,然后返回 Unicode 字符串,如果传入的是字节串形式的路径名,则返回字节串形式的文件名。例如,假设系统默认编码是 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 字符串形式的。


7. 编写 Unicode 感知的程序


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

最重要的一点是:软件只应该在内部使用 Unicode 字符串,当输出时一定要进行某种形式的编码。

如果你想写一个能够同时接受 Unicode 字符串和字节串的函数,你会发现将这两种结合在一起,你的程序很容易产生 Bug。Python 默认的编码是 ASCII,因此每当输入数据中有字节值大于 127的,就会得到 UnicodeDecodeError,因为这类字符无法用 ASCII 编码处理。

这种问题很容易漏掉,如果你使用的测试数据中不包含重音字符,一切看起来都可以工作,但你的程序的确隐藏着一个 Bug,等待第一个使用了码值大于 127字符的用户来触发。所以,第二个建议是:在你的测试数据中包含代码点值 大于 127, 最好有大于 255 的字符。

当数据来源是网页浏览器或其他不受信任的数据源时,常用的一个技术是在执行命令行操作或者写入数据库时进行非法字符检查。如果你正在这样做,请注意检查字符串的最终被使用或存储的形式,因为编码可能被用来进行字符伪装。这在输入数据同时指定了编码方式的情况下更容易出现,许多编码只检查字符,但是 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 的 base64 编码形式,来读取系统文件。上面的代码只在编码形式的字符串中查找 ‘/’,却没有在解码后的字符串中进行检查。


原文:https://docs.python.org/2/howto/unicode.html

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:18613次
    • 积分:375
    • 等级:
    • 排名:千里之外
    • 原创:7篇
    • 转载:4篇
    • 译文:11篇
    • 评论:0条
    文章存档