Python 比较任何类型的序列时,会一一比较序列里的各个元素。对字符串来说,比较的是码位。可是在比较非 ASCII 字符时,得到的结果不尽如人意。
下面对一个生长在巴西的水果的列表进行排序:
>>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] >>> sorted(fruits) ['acerola', 'atemoia', 'açaí', 'caju', 'cajá']
不同的区域采用的排序规则有所不同,葡萄牙语等很多语言按照拉丁字母表排序,重音符号和下加符对排序几乎没什么影响。9 因此,排序时“cajá”视作“caja”,必定排在“caju”前面。
9变音符号对排序有影响的情况很少发生,只有两个词之间唯有变音符号不同时才有影响。此时,带有变音符号的词排在常规词的后面。
排序后的 fruits 列表应该是:
['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
在 Python 中,非 ASCII 文本的标准排序方式是使用 locale.strxfrm 函数,根据 locale 模块的文档(https://docs.python.org/3/library/locale.html?highlight=strxfrm#locale.strxfrm),这 个函数会“把字符串转换成适合所在区域进行比较的形式”。
使用 locale.strxfrm 函数之前,必须先为应用设定合适的区域设置,还要祈祷操作系统支持这项设置。在区域设为 pt_BR 的 GNU/Linux(Ubuntu 14.04)中,可以使用示例 4-19 中的命令。
示例 4-19 使用
locale.strxfrm函数做排序键
>>> import locale >>> locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8') 'pt_BR.UTF-8' >>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] >>> sorted_fruits = sorted(fruits, key=locale.strxfrm) >>> sorted_fruits ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
因此,使用 locale.strxfrm 函数做排序键之前,要调用 setlocale(LC_COLLATE, «your_locale»)。
不过,有几点要注意。
区域设置是全局的,因此不推荐在库中调用 setlocale 函数。应用或框架应该在进程启动时设定区域设置,而且此后不要再修改。
操作系统必须支持区域设置,否则 setlocale 函数会抛出 locale.Error: unsupported locale setting 异常。
必须知道如何拼写区域名称。它在 Unix 衍生系统中几乎已经形成标准,要通过 'language_code.encoding' 获取。10 但是在 Windows 中,句法复杂一些:Language Name-Language Variant_Region Name.codepage。注意,“Language Name”(语言名称)、“Language Variant”(语言变体)和“Region Name”(区域名)中可以包含空格;除了第一部分之外,其他部分的前面是不同的字符:一个连字符、一个下划线和一个点号。除了语言名称之外,其他部分好像都是可选的。例如,English_United States.850,它的语言名称是“English”,区域是“United States”,代码页是“850”。Windows 能理解的语言名称和区域名见于 MSDN 中的文章“Language Identifier Constants and Strings”(https://msdn.microsoft.com/en-us/library/dd318693.aspx),还有“Code Page Identifiers”(https://msdn.microsoft.com/en-us/library/windows/desktop/dd317756(v=vs.85).aspx)一文列出了最后一部分的代码页数字。11
操作系统的制作者必须正确实现了所设的区域。我在 Ubuntu 14.04 中成功了,但在 OS X(Mavericks 10.9)中却失败了。在两台 Mac 中,调用 setlocale(LC_COLLATE, 'pt_BR.UTF-8') 返回的都是字符串 'pt_BR.UTF-8',没有任何问题。但是,sorted(fruits, key=locale.strxfrm) 得到的结果与 sorted(fruits) 一样,是错误的。我还在 OS X 中尝试了 fr_FR、es_ES 和 de_DE,但是 locale.strxfrm 并未起作用。12
10在 Linux 操作系统中,中国大陆的读者可以使用 zh_CN.UTF-8,简体中文会按照汉语拼音顺序进行排序,它也能对葡萄牙语进行正确排序。——编者注
11感谢 Leonardo Rochael,他所做的工作超出了身为技术审校的职责,虽然他是 GNU/Linux 用户,但却研究了这些 Windows 细节。
12同样,我没找到解决方案,不过却发现其他人也报告了同样的问题。本书技术审校之一 Alex Martelli,在他装有 OS X 10.9 的 Mac 电脑中使用 setlocale 和 locale.strxfrm 时没有遇到问题。综上:结果因人而异。
因此,标准库提供的国际化排序方案可用,但是似乎只支持 GNU/Linux(可能也支持 Windows,但你得是专家)。即便如此,还要依赖区域设置,而这会为部署带来问题。
幸好,有个较为简单的方案:PyPI 中的 PyUCA 库。
James Tauber,一位高产的 Django 贡献者,他一定是感受到了这一痛点,因此开发了 PyUCA 库(https://pypi.python.org/pypi/pyuca/),这是 Unicode 排序算法(Unicode Collation Algorithm,UCA)的纯 Python 实现。示例 4-20 展示了它的简单用法。
示例 4-20 使用
pyuca.Collator.sort_key方法
>>> import pyuca >>> coll = pyuca.Collator() >>> fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola'] >>> sorted_fruits = sorted(fruits, key=coll.sort_key) >>> sorted_fruits ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']
这样做更友好,而且恰好可用。我在 GNU/Linux、OS X 和 Windows 中做过测试。目前,PyUCA 只支持 Python 3.x。13
132015 年 5 月,PyUCA 重新支持 Python 2.x,参见:http://jktauber.com/2015/05/13/pyuca-supports-python-2-again。——编者注
PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给 Collator() 构造方法。PyUCA 默认使用项目自带的 allkeys.txt(https://github.com/jtauber/pyuca),这就是 Unicode 6.3.0 的“Default Unicode Collation Element Table”(http://www.unicode.org/Public/UCA/6.3.0/allkeys.txt)的副本。
顺便说一下,那个表是 Unicode 数据库中众多表中的一个。下一节会讨论这个话题。