6.8 Unicode
从Pythonl.6起引进的Unicode字符串支持,是用来在多种双字节字符的格式、编码进行转换的,其中包括一些对这类字符串的操作管理功能。内建的字符串和正则表达式对Unicode字符串的支持,再加上string模块的辅助,Python已经可以应付大部分应用对Unicode的存储、访问、操作的需要了。我们会尽最大的努力把Python对Unicode的支持说清楚,但在这之前,让我们先讨论一些基本的术语,然后问一下自己,到底什么是Unicode。
6.8.1 术语
6.8.2 什么是Unicode
Unicode是计算机可以支持这个星球上多种语言的秘密武器。在Unicode之前,用的都是ASCII。ASCII码非常简单,每个英文字符都是以7位二进制数的方式存贮在计算机内,其范围是32~126。当用户在文件中键入一个大写字符A时,计算机会把A的ASCII码值65写入磁盘,然后当计算机读取该文件时,它会首先把65转化成字符A然后显示到屏幕上。
ASCII编码的文件小巧易读。一个程序只需简单地把文件的每个字节读出来,把对应的数值转换成字符显示出来就可以了。但是ASCII字符只能表示95个可打印字符。后来的软件厂商把ASCII码扩展到了8位,这样一来它就可以多标识128个字符,可是223个字符对需要成千上万的字符的非欧洲语系的语言来说仍然太少。
Unicode通过使用一个或多个字节来表示一个字符的方法突破了ASCII的限制。在这样机制下, Unicode可以表示超过90 000个字符。
6.8.3 怎样使用Unicode
早先,Python只能处理8位的ASCII值,字符串就是简单的数据类型,为了处理一个字符串,用户必须首先创建一个字符串,然后把它作为参数传给string模块的一个函数来处理。2000年,Python 1.6(和2.0)版释出,Unicode第一次在Python里面得到了支持。
为了让Unicode和ASCII码值的字符串看起来尽可能相像,Python的字符串从原来的简单数据类型改成了真正的对象。ASCII字符串成了StringType,而Unicode字符串成了UnicodeType类型。它们的行为是非常相近的。string模块里面都有相应的处理函数。string模块已经停止了更新,只保留了ASCII码的支持,string模块已经不推荐使用,在任何需要跟Unicode兼容的代码里都不要再用该模块,Python保留该模块仅仅是为了向后兼容。
Python里面处理Unicode字符串跟处理ASCII字符串没什么两样。Python把硬编码的字符串叫做字面上的字符串,默认所有字面上的字符串都用ASCII编码,可以通过在字符串前面加一个‘u’前缀的方式声明Unicode字符串,这个‘u’前缀告诉Python后面的字符串要编码成Unicode字符串。
内建的str()函数和chr()函数并没有升级成可以处理Unicode。它们只能处理常规的ASCII编码字符串,如果一个Unicode字符串被作为参数传给了str()函数,它会首先被转换成ASCII字符串然后在交给str()函数。如果该Unicode字符串中包含任何不被ASCII字符串支持的字符,会导致str()函数报异常。同样地,chr()函数只能以0~255作为参数工作。如果你传给它一个超出此范围的值(比如说一个Unicode字符),它会报异常。
新的内建函数unicode()和unichar()可以看成Unicode版本的str()和chr()。Unicode()函数可以把任何Python的数据类型转换成一个Unicode字符串,如果是对象,并且该对象定义了unicode()方法,它还可以把该对象转换成相应的Unicode字符串。具体内容见6.1.3和6.5.3节。
6.8.4 Codec是什么
codec是COder/DECoder的首字母组合。它定义了文本跟二进制值的转换方式,跟ASCII那种用一个字节把字符转换成数字的方式不同,Unicode用的是多字节。这导致了Unicode支持多种不同的编码方式。比如说codec支持的4种耳熟能详的编码方式:ASCII、ISO 8859-1/Latin-1、UTF-8和UTF-16。
其中最著名的是UTF-8编码,它也用一个字节来编码ASCII字符,这让那些必须同时处理ASCII码和Unicode码文本的程序员的工作变得非常轻松,因为ASCII字符的UTF-8编码跟ASCII编码完全相同。
UTF-8编码可以用1~4个字节来表示其他语言的字符,CJK/East这样的东亚文字一般都是用3个字节来表示,那些少用的、特殊的或者历史遗留的字符用4个字节来表示。这给那些需要直接处理Unicode数据的程序员带来了麻烦,因为他们没有办法按照固定长度逐一读出各个字符。幸运的是我们不需要掌握直接读写Unicode数据的方法,Python已经替我们完成了相关细节,我们无须为处理多字节字符的复杂问题而担心。Python里面的其他编码不是很常用,事实上,我们认为大部分的Python程序员根本就用不着去处理其他的编码,UTF-16可能是个例外。
UTF-16可能是以后大行其道的一种编码格式,它容易读写,因为它把所有的字符都是用单独的一个16位字,两个字节来存储的,正因为此,这两个字节的顺序需要定义一下,一般的UTF-16编码文件都需要一个BOM(位顺序标记,Byte Order Mark),或者你显式地定义UTF-16-LE(小端)或者UTF-16-BE (大端)字节序。
从技术上讲,UTF-16也是一种变长编码,但它不是很常用(人们一般不会知道或者根本不在意除了基本多文种平面BMP之外到底使用的是那种平面),尽管如此,UTF-16并不向后兼容ASCII,因此,实现它的程序很少,因为大家需要对ASCII进行支持。
6.8.5 编码解码
Unicode支持多种编码格式,这为程序员带来了额外的负担,每当你向一个文件写入字符串的时候,你必须定义一个编码(encoding参数)用于把对应的Unicode内容转换成你定义的格式,Python通过Unicode字符串的encode()函数解决了这个问题,该函数接受字符串中的字符为参数,输出你指定的编码格式的内容。
所以,每次我们写一个Unicode字符串到磁盘上我们都要用指定的编码器给他“编码”一下。相应地,当我们从这个文件读取数据时,我们必须“解码”该文件,使之成为相应的Unicode字符串对象。
1. 简单的例子
下面的代码创建了一个Unicode字符串,用UTF-8编码器将它编码,然后写入到一个文件中去。接着把数据从文件中读回来,解码成Unicode字符串对象。最后,打印出Unicode字符串,用以确认程序正确地运行。
2. 逐行解释
第1 ~ 7行
像通常一样,首先定义了doc字符串和用以表示解码器的常量,还有用以存储字符串的文件名。
第9 ~ 19行
我们创建了一个Unicode字符串,用我们指定的编码格式对其进行编码,然后把它写入到文件中去,(9-13行),接着我们把内容从文件中重新读出来。解码,显示到屏幕上,输出的时候去掉print的自动换行,因为我们已经在字符串中写了一个换行符(15~19行)。
例6.2 简单Unicode字符串例子(uniFile.py)
这个简单的例子中,我们把一个Unicode字符串写入到磁盘文件,然后再把它读出并显示出来。写入的时候用UTF-8编码,读出也一样,用UTF-8。
运行该程序,我们得到如下的输出。
在文件系统中也会发现一个叫unicode.txt的文件,里面包含跟输出的内容一致的数据。
3. 简单Web例子
在第20章Web编程里面我们展示了一个简单的在CGI应用中使用Unicode的例子。
6.8.6 把Unicode应用到实际应用中
这些处理Unicode字符串的例子简单到让人感到有点假,事实上,只要你遵守以下的规则,处理 Unicode就是这么简单。
程序中出现字符串时一定要加个前缀u。
不要用str()函数,用unicode()代替。
不要用过时的string模块——如果传给它的是非ASCII字符,它会把一切搞砸。
不到必须时不要在你的程序里面编解码Unicod字符。只在你要写入文件或数据库或者网络时,才调用encode()函数;相应地,只在你需要把数据读回来的时候才调用decode()函数。
这些规则可以规避90%由于Unicode字符串处理引起的bug。现在的问题是剩下的10%的问题却让你处理不了,幸亏Python提供了大量的模块、库来替你处理这些问题。它们可以让你用10行Python语句写出其他语言需要100行语句才能完成的功能,但是相应地,对Unicode支持的质量也完全取决于这些模块、库。
Python标准库里面的绝大部分模块都是兼容Unicode的,除了pickle模块!pickle模块只支持ASCII字符串。如果你把一个Unicode字符串交给pickle模块来unpickle,它会报异常。你必须先把你的字符串转换成ASCII字符串才可以。所以最好是避免基于文本的pickle操作。幸运地是现在二进制格式已经作为pickle的默认格式了,pickle的二进制格式支持不错。这点在你向数据库里面存东西是尤为突出,把它们作为BLOB字段存储而不是作为TEXT或者VARCHAR字段存储要好很多。万一有人把你的字段改成了Unicode类型,这可以避免pickle的崩溃。
如果你的程序里面用到了很多第三方模块,那么你很可能在各个模块统一使用Unicode通讯方面遇到麻烦,Unicode还没成为一项必须的规定,在你系统里面的第三方模块(包括你的应用要面对的平台\系统)需要用相同的Unicode编码,否则,可能你就不能正确的读写数据。
作为一个例子,假设你正在构建一个用数据库来读写Unicode数据的Web应用。为了支持Unicode,你必须确保以下方面对Unicode的支持。
数据库服务器(MySQL、PostgreSQL、SQL Server等)
数据库适配器(MySQLdb等)
Web开发框架(mod_python、cgi,Zope、Plane、Django等)
数据库方面最容易对付,你只要确保每张表都用UTF-8编码就可以了。
数据库适配器可能有点麻烦,有些适配器支持Unicode而有些不支持。比如说MySQLdb,它并不是默认就支持Unicode模式,你必须在connect()方法里面用一个特殊的关键字use_unicode来确保你得到的查询结果是Unicode字符串。mod_python里面开启对Unicode的支持相当简单,只要在request对象里面把text-encoding一项设成“utf-8”就行了,剩下的mod_python都会替你完成,Zope等其他复杂的系统可能需要更多的工作来支持Unicode。
6.8.7 从现实中得来的教训
失误#1:你必须在一个极有限的时间内写出一个大型的应用,而且需要其他语言的支持,但是产品经理并没有明确定义这一点。你并没有考虑Unicode的兼容,直到项目快要结束……这时候再添加Unicode的支持几乎不太可能,不是吗?
结果#1:没能预测到最终用户对其他语言界面的需求,在集成他们用的面向其他语种的应用时又没有使用Unicode支持。更新整个系统既让人觉得枯燥,又浪费时间。
失误#2:在源码中到处使用string模块或者str()和chr()函数。
结果#2:通过全局的查找替换把str()和chr()替换成unicode()和unichr(),但是这样一来很可能就不能再用pickle模块,要用的话只能把所有要pickle处理的数据存成二进制形式,这样一来就必须修改数据库的结构,而修改数据库结构就意味着全部推倒重来。
失误#3:不能确定所有的辅助系统都完全地支持Unicode。
结果#3:不得不去为那些系统打补丁,而其中有些系统可能你根本就没有源码。修复对Unicode支持的bug可能会降低代码的可靠性,而且非常有可能引入新的bug。
总结:使应用程序完全支持Unicode,兼容其他的语言本身就是一个工程。
它需要详细的考虑、计划。所有涉及的软件、系统都需要检查,包括Python的标准库和其他将要用到的第三方扩展模块。你甚至有可能需要组建一个经验丰富的团队来专门负责国际化(I18N)问题。
6.8.8 Python的Unicode支持
1. 内建的unicode()函数
Unicode的工厂方法,同Unicode字符串操作符(u/U)的工作方式很类似,它接受一个string做参数,返回一个Unicode字符串。
2. 内建的decode()/encode()方法
decode()和encode()内建函数接受一个字符串做参数返回该字符串对应的解码后/编码后的字符串。 decode()和encode()都可以应用于常规字符串和Unicode字符串。decode()方法是在Python2.2以后加入的。
3. Unicode类型
Unicode字符串对象是basestring的子类、用Unicode()工厂方法或直接在字符串前面加一个u或者U来创建实例。支持Unicode原始字符串,只要在你的字符串前面加一个ur或者UR就可以了。
4. Unicode序数
标准内建函数ord()工作方式相同,最近已经升级到可以支持Unicode对象了。内建的unichr()函数返回一个对应的Unicode字符(需要一个32位的值);否则就产生一个ValueError异常。
5. 强制类型转换
混合类型字符串操作需要把普通字符串转换成Unicode对象。
6. 异常
UnicodeError异常是在exceptions模块中定义的,ValueError的子类。所有关于Unicode编解码的异常都要继承自UnicodeError。详见encode()函数。
7. 标准编码
表6.9简洁地列出了Python中常用的编码方式。更详细、完全的列表见Python文档,下面是它的链接。
8. RE引擎对Unicode的支持
正则表达式引擎需要Unicode支持。详见6.9节的re模块。
9. 字符串格式化操作符
对于Python的格式化字符串的操作符,%s把Python字符串中的Unicode对象执行了str(u)操作,所以,输出的应该是u.encode(默认编码)。如果格式化字符串是Unicode对象,所有的参数都将首先强制转换成Unicode然后根据对应的格式串一起进行格式转换。数字首先被转换成普通字符串,然后在转换成Unicode。Python字符串通过默认编码格式转化成Unicode。Unicode对象不变,所有其他格式字符串都需要像上面这样转化,下面是例子。