Python不能正确地排序Unicode。Strcoll没有帮助。

26

我在使用Python 2.5.1和2.6.5在OSX和Linux系统上对包含Unicode字符的列表进行排序时遇到了问题。

import locale   
locale.setlocale(locale.LC_ALL, 'pl_PL.UTF-8')
print [i for i in sorted([u'a', u'z', u'ą'], cmp=locale.strcoll)]

应该打印:

[u'a', u'ą', u'z']

但实际上打印输出的是:

[u'a', u'z', u'ą']

总的来说,看起来strcoll出了问题。我已经尝试过各种类型的变量(例如非Unicode编码的字符串)。

我做错了什么?

最好的祝福, Tomasz Kopczuk。


1
在你的 setlocale 行之后,locale.getlocale(LC_COLLATE) 返回什么? - Amber
locale 模块使用 C 库的 locale API,因此如果出现错误,则必须在 C 库中。使用 locale de_DE.UTF-8 和字符串 ä 而不是 ą 进行等效测试可以正常工作。即使我使用带有 ą 的德语区域设置,顺序也是正确的,因此 C 库中的波兰语区域实现可能存在问题。作为解决方法,您可以使用 unicodedata.normalize 将字符串转换为规范化形式 D,然后即使是简单的 strcmp 排序也应该可以工作。 - Philipp
好的,我也对此很感兴趣。我尝试了pl_PL.UTF-8de_DE.UTF-8,还尝试了在OS X上使用sort(key=locale.strxfrm)而不是使用strcoll,但目前我得到的结果与你的不正确。在de_DE.UTF8中,字符ä对我来说无法正常工作。 - chryss
2
在Linux上可以运行,但在Mac上不行。也许OS X的排序表有问题,或者其他什么原因?顺便说一下,对于Web应用程序来说,POSIX语言环境不可靠,因为它们是基于进程而非线程安全的。 - bobince
1
在 Linux(Ubuntu)上对我有效,但在 Mac 和 FreeBSD 上无效。 - viam0Zah
8个回答

18

显然,使排序在所有平台上正常工作的唯一方法是使用ICU库和PyICU绑定(PyPI上的PyICU)。

在Mac OS X上: sudo port install py26-pyicu,需要注意这里描述的错误:https://svn.macports.org/ticket/23429 (哦,使用macports的喜悦)。

不幸的是,PyICU的文档非常缺乏,但我还是找到了如何做到这一点:

import PyICU
collator = PyICU.Collator.createInstance(PyICU.Locale('pl_PL.UTF-8'))
print [i for i in sorted([u'a', u'z', u'ą'], cmp=collator.compare)]

生成以下结果:

[u'a', u'ą', u'z']

另一个专业人士 - @bobince:它是线程安全的,因此在设置请求区域设置时并不无用。


2
好问题,好答案——如果你在波兰,那么你已经领先了几步,这一点也不奇怪。无论如何,这已经是我第二次看到 Python 依赖底层 C 库的问题了。你知道这些问题可以在哪里提出吗? - chryss
我认为这可能是库本身的问题,而不是Python的问题。但正如gnibbler指出的那样 - 它在某些操作系统中可以工作,所以也许,至少这个特定的问题已经在某个时候得到了解决。OS X以使用旧版gcc而闻名,而我测试的另一个操作系统是Fedora 8 - 它本身并不是很现代化。我会在其中一个基础C库的邮件列表中提出这个问题。干杯,伙计 :) - Tomek Kopczuk
2
我同意。我创建了一个Gist http://gist.github.com/509520 并将其提供给一些人尝试。我喜欢 i18n,但是错误使它变得繁琐。 - chryss

6
只是补充tkopczuk的调查:这绝对是一个gcc的bug,至少对于OS X 10.6.4上的版本4.2.1而言。可以通过直接调用C strcoll() 来重现此问题,如此片段所示。
编辑:仍然在同一系统上,我发现对于de_DE、fr_FR、pl_PL的UTF-8版本,存在问题,但对于fr_FR和de_DE的ISO-88591版本,排序顺序是正确的。不幸的是,对于ISO-88592的pl_PL也有缺陷。
The order for Polish ISO-8859 is:
LATIN SMALL LETTER A
LATIN SMALL LETTER Z
LATIN SMALL LETTER A WITH OGONEK
The LC_COLLATE culture and encoding settings were pl_PL, ISO8859-2.

The order for Polish Unicode is:
LATIN SMALL LETTER A
LATIN SMALL LETTER Z
LATIN SMALL LETTER A WITH OGONEK
The LC_COLLATE culture and encoding settings were pl_PL, UTF8.

The order for German Unicode is:
LATIN SMALL LETTER A
LATIN SMALL LETTER Z
LATIN SMALL LETTER A WITH DIAERESIS
The LC_COLLATE culture and encoding settings were de_DE, UTF8.

The order for German ISO-8859 is:
LATIN SMALL LETTER A
LATIN SMALL LETTER A WITH DIAERESIS
LATIN SMALL LETTER Z
The LC_COLLATE culture and encoding settings were de_DE, ISO8859-1.

The order for Fremch ISO-8859 is:
LATIN SMALL LETTER A
LATIN SMALL LETTER E WITH ACUTE
LATIN SMALL LETTER Z
The LC_COLLATE culture and encoding settings were fr_FR, ISO8859-1.

The order for French Unicode is:
LATIN SMALL LETTER A
LATIN SMALL LETTER Z
LATIN SMALL LETTER E WITH ACUTE
The LC_COLLATE culture and encoding settings were fr_FR, UTF8.

1
能否将/usr/share/locale/pl_PL.UTF-8/LC_COLLATE反编译成可读形式?可能问题不在于gcc,而是像@bobince指出的错误排序表。 - Tomek Kopczuk
我在德语和法语上得到了相同的行为(即,带有变音符号的字符在“z”之后排序),因此这不仅仅是波兰排序表的问题。我想知道它是否只选择C语言环境或者可能是默认语言环境(我的是en_GB--你的是pl_PL吗?)。无论如何,显然它在C库中,无论是在数据还是代码中,我都无法确定。 - chryss
是的,我的是pl_PL。但检查排序表并确保它们是合法的会很好,然后还有使用库时不同区域设置的问题。但我猜这是库的问题,因此在各种操作系统上都会出现问题。 - Tomek Kopczuk
我不知道平台特定的排序表是如何制作的,除了它们应该是由公共语言环境存储库http://cldr.unicode.org/创建的。我对此越深入研究,就越认为C库只是一种非常基本的考虑本地化的方式,而且在进行严格的工作时最好使用ICU。经过更多测试,de_DE和fr_FR ISO语言环境可行,但pl_PL的ISO存在错误。 - chryss
这个问题似乎也适用于其他德语区域设置,即de_ATde_CH以及de_DE的“独立”和UTF-8版本。ISO8859-1ISO8859-15看起来没问题。操作系统:OS X 10.10.5(Yosemite)。 - Kay

5

以下是我如何在不使用 PyICU 的情况下正确地排序波斯语(使用 Python 3.x):

首先设置区域设置(别忘了导入 localeplatform

if platform.system() == 'Linux':
    locale.setlocale(locale.LC_ALL, 'fa_IR.UTF-8')
elif platform.system() == 'Windows':
   locale.setlocale(locale.LC_ALL, 'Persian_Iran.1256')
else:
   pass (or any other OS)

然后使用 key 排序:

a = ['ا','ب','پ','ت','ث','ج','چ','ح','خ','د','ذ','ر','ز','ژ','س','ش','ص','ض','ط','ظ','ع','غ','ف','ق','ک','گ','ل','م','ن','و','ه','ي']

print(sorted(a,key=locale.strxfrm))

对象列表:

a = [{'id':"ا"},{'id':"ب"},{'id':"پ"},{'id':"ت"},{'id':"ث"},{'id':"ج"},{'id':"چ"},{'id':"ح"},{'id':"خ"},{'id':"د"},{'id':"ذ"},{'id':"ر"},{'id':"ز"},{'id':"ژ"},{'id':"س"},{'id':"ش"},{'id':"ص"},{'id':"ض"},{'id':"ط"},{'id':"ظ"},{'id':"ع"},{'id':"غ"},{'id':"ف"},{'id':"ق"},{'id':"ک"},{'id':"گ"},{'id':"ل"},{'id':"م"},{'id':"ن"},{'id':"و"},{'id':"ه"},{'id':"ي"}]

print(sorted(a, key=lambda x: locale.strxfrm(x['id']))

最后,您可以返回地区设置信息:
locale.setlocale(locale.LC_ALL, '')

4

@gnibbler,使用PyICU和sorted()函数可以在Python3环境下工作。通过查阅ICU API文档并进行一些实验,我找到了getSortKey()函数:

import PyICU
collator = PyICU.Collator.createInstance(PyICU.Locale('de_DE.UTF-8'))
sorted(['a','b','c','ä'],key=collator.getSortKey)

生成所需的排序方式:

['a', 'ä', 'b', 'c']

替换不需要的排序方式:

sorted(['a','b','c','ä'])
['a', 'b', 'c', 'ä']

2
import locale
from functools import cmp_to_key
iterable = [u'a', u'z', u'ą']
sorted(iterable, key=cmp_to_key(locale.strcoll))  # locale-aware sort order

(参考链接: http://docs.python.org/3.3/library/functools.html)

这是一个关于Python标准库中functools模块的文档。functools模块提供了函数式编程工具,以便更好地支持高阶函数。其中包括函数缓存、偏函数和其他操作,这些可以让我们更容易地使用一些常见的函数编程模式。

1

自2012年以来,有一种名为natsort的库。它包括了许多惊人的函数,如natsortedhumansorted。更重要的是,它们不仅适用于列表! 代码:

from natsort import natsorted, humansorted

lst = [u"a", u"z", u"ą"]
dct = {"ą": 1, "ż": 3, "Ż": 4, "b": 5}

lst_natsorted = natsorted(lst)
lst_humansorted = humansorted(lst)
dct_natsorted = dict(natsorted(dct.items()))
dct_humansorted = dict(humansorted(dct.items()))

print("List natsorted: ", lst_natsorted)
print("List humansorted: ", lst_humansorted, "\n")
print("Dictionary natsorted: ", dct_natsorted)
print("Dictionary humansorted: ", dct_humansorted)

输出:

List natsorted:  ['a', 'ą', 'z']
List humansorted:  ['a', 'ą', 'z']

Dictionary natsorted:  {'Ż': 4, 'ą': 1, 'b': 5, 'ż': 3}  
Dictionary humansorted:  {'ą': 1, 'b': 5, 'ż': 3, 'Ż': 4}

如您所见,排序字典时结果会有所不同,但考虑到给定的列表,这两个结果都是正确的。

顺便说一下,这个库也很适合对包含数字的字符串进行排序:

from natsort import natsorted, humansorted

lst_mixed = ["a9", "a10", "a1", "c4", "c40", "c5"]

mixed_sorted = sorted(lst_mixed)
mixed_natsorted = natsorted(lst_mixed)
mixed_humansorted = humansorted(lst_mixed)

输出:

List with mixed strings sorted:  ['a1', 'a10', 'a9', 'c4', 'c40', 'c5']
List with mixed strings natsorted:  ['a1', 'a9', 'a10', 'c4', 'c5', 'c40']
List with mixed strings humansorted:  ['a1', 'a9', 'a10', 'c4', 'c5', 'c40']

0
在 Ubuntu Lucid 上,cmp 排序似乎正常工作,但输出编码错误。
>>> import locale   
>>> locale.setlocale(locale.LC_ALL, 'pl_PL.UTF-8')
'pl_PL.UTF-8'
>>> print [i for i in sorted([u'a', u'z', u'ą'], cmp=locale.strcoll)]
[u'a', u'\u0105', u'z']

使用 locale.strxfrm 的关键字不起作用,除非我漏了什么

>>> print [i for i in sorted([u'a', u'z', u'ą'], key=locale.strxfrm)]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character u'\u0105' in position 0: ordinal not in range(128)

使用strxfrm函数时,据我所知,您需要手动解码Unicode字符串。 - Tomek Kopczuk
2
@tkopczuk,如果能找到一种方式用key作为排序依据就好了,因为在Python3中已经没有了cmp参数可以使用。 - John La Rooy
1
它似乎与提供的functools.cmp_to_key函数(from functools import cmp_to_key)一起正常工作,就像这样:sorted([u'a', u'z', u'ą'], key=cmp_to_key(collator.compare)) - Tomek Kopczuk

0
一个老问题,但需要一些澄清。对于Python中的区域敏感排序,有两种方法可用。你采用哪种方法取决于你使用的操作系统。
第一种方法是使用内置的locale模块。这将取决于你所在的操作系统和可用的区域设置。
import locale
locale.setlocale(locale.LC_COLLATE, 'pl_PL.UTF-8')
test_list = ['a', 'z', 'ą']
sorted(test_list, key=locale.strxfrm)

如果我使用的是使用glibc的Linux版本,我将得到['a', 'ą', 'z']
如果我使用的是使用Musl libc的Linux版本,或者是为嵌入式系统开发的Linux发行版,我将得到['a', 'z', 'ą'],即不支持区域敏感排序。
如果我使用的是基于BSD libc的系统(如macOS),我将得到['a', 'z', 'ą']
在macOS上,如果运行以下命令:
ls -al  /usr/share/locale/pl_PL/LC_COLLATE

你会得到/usr/share/locale/pl_PL/LC_COLLATE -> ../la_LN.US-ASCII/LC_COLLATE,也就是说,波兰排序表被符号链接到另一个排序表,从而创建了一种语言无关的排序。这与其他基于BSD libc的系统类似,在文件系统中优先考虑了稳定的区域设置独立排序。
第二种方法是针对安装了icu4c的系统,使用PyICU。ICU4C使用通用区域设置数据存储库(CLDR)。CLDR区域设置数据比基于libc的实现中的区域设置数据更详尽。
import icu
collator = icu.Collator.createInstance(icu.Locale('pl'))
sorted(test_list, key=collator.getSortKey)

给出['a', 'ą', 'z']
地区数据因实现而异,这不仅影响排序,还会在其他与地区敏感操作中可见。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接