在Python字符串中忽略大小写

53

在Python中,忽略大小写比较字符串最简单的方法是什么?

当然,可以使用(str1.lower() <= str2.lower())等方式,但这会创建两个额外的临时字符串(伴随着明显的分配/垃圾回收开销)。

我想找到类似于C中的stricmp()的等效函数。

[有更多上下文要求,所以我将用一个简单的例子演示:]

假设您想对一长串字符串进行排序。 您只需执行theList.sort()即可。 这是O(n * log(n))字符串比较,并且没有内存管理(因为所有字符串和列表元素都是某种智能指针)。您很高兴。

现在,您想要完成相同的操作,但要忽略大小写(让我们简化并说所有字符串都是ASCII,因此可以忽略语言环境问题)。 您可以执行theList.sort(key=lambda s: s.lower()),但这样会导致每次比较产生两个新的分配,以及将重复的(小写)字符串加重垃圾回收器的负担。 每个这样的内存管理噪声比简单的字符串比较慢几个数量级。

现在,通过一个类似于stricmp()的原地函数,您可以执行:theList.sort(cmp=stricmp),它与theList.sort()一样快且内存友好。您又很高兴了。

问题在于,任何基于Python的大小写不敏感比较都会涉及隐式字符串复制,因此我希望能找到基于C的比较(也许在模块字符串中)。

没有找到类似于那样的东西,因此在这里提出了问题。 (希望这澄清了问题)。


PHP等效函数:strcasecmp - http://nl3.php.net/strcasecmp - fijter
4
你的假设是错误的。使用 key= 参数的 list.sort() 并不意味着“每比较一次就进行两次新的分配”。(相反,使用 cmp= 参数的 list.sort() 确实会在每次比较时调用该参数。) - user3850
尝试重命名问题,从“在Python字符串中忽略大小写”更改为“Python中7位ASCII字符串比较的最接近stricmp是什么?”以更准确地反映原始发帖者的实际问题。主要问题:Unicode也是“字符串”,但这个问题会让他们完全错误,请参见tchrist的评论。 - n611x007
请问在 Python 2 中怎么做字符串大小写折叠?(原文链接:https://dev59.com/k2Ml5IYBdhLWcg3wmn_R) - jfs
16个回答

74
这里有一个基准测试,显示使用 str.lower 比接受的答案提出的方法(libc.strcasecmp)更快:
#!/usr/bin/env python2.7
import random
import timeit

from ctypes import *
libc = CDLL('libc.dylib') # change to 'libc.so.6' on linux

with open('/usr/share/dict/words', 'r') as wordlist:
    words = wordlist.read().splitlines()
random.shuffle(words)
print '%i words in list' % len(words)

setup = 'from __main__ import words, libc; gc.enable()'
stmts = [
    ('simple sort', 'sorted(words)'),
    ('sort with key=str.lower', 'sorted(words, key=str.lower)'),
    ('sort with cmp=libc.strcasecmp', 'sorted(words, cmp=libc.strcasecmp)'),
]

for (comment, stmt) in stmts:
    t = timeit.Timer(stmt=stmt, setup=setup)
    print '%s: %.2f msec/pass' % (comment, (1000*t.timeit(10)/10))

我的电脑上的典型时间:

235886 words in list
simple sort: 483.59 msec/pass
sort with key=str.lower: 1064.70 msec/pass
sort with cmp=libc.strcasecmp: 5487.86 msec/pass

因此,使用 str.lower 版本不仅远远是最快的,而且也是所有在此处提出的解决方案中最具可移植性和 Python 风格的。我没有分析内存使用情况,但原始帖子仍未给出一个令人信服的理由来担心它。此外,谁说 libc 模块的调用不会复制任何字符串?
注意: lower() 字符串方法还具有依赖于区域设置的优点。当编写自己的“优化”解决方案时,您可能无法正确处理某些内容。即便如此,在 Python 中由于错误和缺失的功能,这种比较可能在 Unicode 上下文中给出错误的结果。

3
当然,内存是一个问题,因为超过99.9%的.lower()时间都用于内存分配。此外,在我检查的(Windows)机器上,使用key=_stricmp方法要快4-5倍,并且没有内存惩罚。 - Paul Oyster
4
比使用.lower()方法快4-5倍意味着它比简单排序的情况快2倍。这怎么可能?!? - user3850
3
如果你不使用 Unicode 大小写折叠,将会得到各种错误的答案,这也是错误的。 - tchrist
2
@hop:请查看bugs.python.org,以了解Unicode错误。我刚刚上传了一堆测试用例,展示了Python在不使用大小写折叠时出现的问题。如果我必须在速度和正确性之间选择,我知道我会每次都选择哪一个。 - tchrist
7
最好避免用“愚蠢”来形容别人的答案。 - Chris Dutrow
显示剩余17条评论

7

您是否在高性能敏感应用程序的非常频繁执行路径中使用此比较?或者,您是否在运行大小为兆字节的字符串上运行此操作?如果没有,那么您不必担心性能问题,只需使用 .lower() 方法。

以下代码演示了通过在两个大小接近 1MB 的字符串上调用 .lower() 进行大小写不敏感比较所花费的时间,大约需要 0.009 秒,在我的 1.8GHz 台式计算机上。

from timeit import Timer

s1 = "1234567890" * 100000 + "a"
s2 = "1234567890" * 100000 + "B"

code = "s1.lower() < s2.lower()"
time = Timer(code, "from __main__ import s1, s2").timeit(1000)
print time / 1000   # 0.00920499992371 on my machine

如果这确实是一个非常重要且性能关键的代码段,我建议您在C语言中编写一个函数并从Python代码中调用它,因为这将允许您进行真正高效的不区分大小写的搜索。有关编写C扩展模块的详细信息,请参见此处:https://docs.python.org/extending/extending.html


3
这就是如何将东西传递给计时器类。感谢您解决了我的一个非常不同的问题 :) - Manav
5
这是完全错误的。它未能检测到 ΣΤΙΓΜΑΣστιγμας 是大小写不敏感的相同形式。您不能使用Unicode中的大小写映射来比较大小写,必须使用大小写折叠。这些是不同的事情。Σ、σ、ς 都一样,就像 S、ſ、s(s是什么鬼?:))和 Μ、μ、µ 一样。还有无数其他类似的情况,比如 weiß、WEIẞ、weiss、WEISS 也都是一样的,或者 efficient、efficient。 您必须使用大小写折叠,因为大小写映射不起作用。 - tchrist

7

你的问题意味着你不需要使用Unicode。请尝试以下代码片段;如果可以正常工作,那么你就完成了:

Python 2.5.2 (r252:60911, Aug 22 2008, 02:34:17)
[GCC 4.3.1] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_COLLATE, "en_US")
'en_US'
>>> sorted("ABCabc", key=locale.strxfrm)
['a', 'A', 'b', 'B', 'c', 'C']
>>> sorted("ABCabc", cmp=locale.strcoll)
['a', 'A', 'b', 'B', 'c', 'C']

澄清:如果一开始不太明显,locale.strcoll似乎是你需要的函数,避免使用str.lower或locale.strxfrm“重复”的字符串。

4
locale.setlocale() 的全局设置显然过于宏大(范围太大了)。 - Paul Oyster
我不知道什么是“明显过度”,而“全局”设置可以根据您的喜好进行本地化(除非您正在处理线程并且因某些原因需要对某些线程进行本地化)。 - tzot
1
这是唯一的解决方案,可以与不区分大小写的实用程序(如带有-f选项的Unix sort)正确地进行交互。例如,str.lower会导致A_在AA之前排序。 - Neil Mayhew
3
您不能使用基于POSIX的语言环境和strcoll函数,因为它在各个平台上都不可靠。您必须使用Unicode大小写折叠,这在任何地方都保证能够正常工作。 - tchrist

6
我无法找到其他内置的不区分大小写比较方法:Python食谱配方使用lower()。
但是,当使用lower进行比较时,必须小心,因为存在土耳其I问题。不幸的是,Python对土耳其的处理不好。 ı 转换为 I,但 I 不转换为 ı。 İ 转换为 i,但 i 不转换为 İ。

4
Python 对 Unicode 的处理不是非常健壮,你已经看到了。大小写转换并没有考虑这些问题。非常遗憾。 - tchrist

3

没有内置等价于您想要的那个函数。

您可以编写自己的函数,逐个字符转换为 .lower(),以避免复制两个字符串,但我确定它将非常耗费 CPU,并且效率极低。

除非您正在使用非常长的字符串(如果重复可能会导致内存问题),否则我建议保持简单并使用

str1.lower() == str2.lower()

你会没事的


2
“永远不要说永远” :) “没有内置的等价物”是绝对的;“我不知道有任何内置的等价物”更接近事实。locale.strcoll,如果给定一个不区分大小写的LC_COLLATE(如'en_US'),则是内置的。 - tzot
2
这个答案是错误的。唯一正确的方法是 str1.fold() == str2.fold(),但这需要一个扩展默认 Python 字符串类的函数,支持字符串的完整 Unicode casefold。这是一个缺失的函数。 - tchrist
@tchrist unclearr:是否有这样的扩展可用? - n611x007

2

这个问题涉及到两个不同的方面:

  1. 在Python中,忽略大小写,最简单的比较字符串的方法是什么?
  2. 我想找一个类似于C语言中的stricmp()函数的等价物。

由于问题#1已经有非常好的答案了(即:str1.lower() < str2.lower()),所以我来回答问题#2。

def strincmp(str1, str2, numchars=None):
    result = 0
    len1 = len(str1)
    len2 = len(str2)
    if numchars is not None:
        minlen = min(len1,len2,numchars)
    else:
        minlen = min(len1,len2)
    #end if
    orda = ord('a')
    ordz = ord('z')

    i = 0
    while i < minlen and 0 == result:
        ord1 = ord(str1[i])
        ord2 = ord(str2[i])
        if ord1 >= orda and ord1 <= ordz:
            ord1 = ord1-32
        #end if
        if ord2 >= orda and ord2 <= ordz:
            ord2 = ord2-32
        #end if
        result = cmp(ord1, ord2)
        i += 1
    #end while

    if 0 == result and minlen != numchars:
        if len1 < len2:
            result = -1
        elif len2 < len1:
            result = 1
        #end if
    #end if

    return result
#end def

只有在有意义的情况下才使用此函数,因为在许多情况下小写技术将更优。

我只处理ASCII字符串,不确定这在Unicode上的行为如何。


2
当标准库中没有很好的支持时,我总是寻找PyPI软件包。随着虚拟化和现代Linux发行版的普及,我不再避免使用Python扩展程序。 PyICU似乎符合要求:https://dev59.com/cnNA5IYBdhLWcg3wF5qO#1098160
现在也有一个纯Python的选项。它经过了充分测试:https://github.com/jtauber/pyuca
旧答案: 我喜欢正则表达式的解决方案。这里是一个函数,你可以将其复制并粘贴到任何函数中,感谢Python的块结构支持。
def equals_ignore_case(str1, str2):
    import re
    return re.match(re.escape(str1) + r'\Z', str2, re.I) is not None

因为我使用的是match而不是search,所以在正则表达式中不需要加入插入符(^)。

注意:这只检查相等性,有时候这就是所需的。我也不敢说我喜欢它。


我希望有一个虚拟橡皮图章来解决这个问题。不要使用 $,改用\Z。阅读优秀的手册,了解 $ 的实际作用;不要仅凭传说、猜测或其他东西来依赖它。 - John Machin
我改变了它。我还为我的回答启用了社区Wiki功能。谢谢。 - Benjamin Atkin
只适用于相等测试,这与比较两个字符串并确定一个是否小于、等于或大于另一个并不完全相同。 - martineau
@martineau 谢谢。我添加了一条备注,并且进行了一些搜索,找到了一个我认为更加舒适的解决方案,并在我的答案中更新了它。虽然这不是完整的答案,但希望有人(如果我有时间的话可以自己做)学习其中一个库的工作原理并提供代码示例。 - Benjamin Atkin
1
是的,听起来 pyuca(Python Unicode Collation Algorithm)扩展可能适用,因为它基于的报告——Unicode 排序算法 (UCA)——表示“大小写差异(大写与小写之间),通常会被忽略”。 - martineau

1

这是使用re的方法:

import re
p = re.compile('^hello$', re.I)
p.match('Hello')
p.match('hello')
p.match('HELLO')

不区分大小写的正则表达式只能用于相等性测试(True/False),而不能用于比较(小于/等于/大于)。 - tzot

1

对于使用计算成本高昂的键来排序值列表的推荐惯用语是所谓的“装饰模式”。它简单地由原始列表构建一个(键,值)元组列表,然后对该列表进行排序。然后轻松消除键并获取已排序值的列表:

>>> original_list = ['a', 'b', 'A', 'B']
>>> decorated = [(s.lower(), s) for s in original_list]
>>> decorated.sort()
>>> sorted_list = [s[1] for s in decorated]
>>> sorted_list
['A', 'a', 'B', 'b']

或者如果你喜欢一行代码:

>>> sorted_list = [s[1] for s in sorted((s.lower(), s) for s in original_list)]
>>> sorted_list
['A', 'a', 'B', 'b']

如果你真的担心调用lower()的成本,你可以在任何地方存储(lowered string, original string)元组。元组是Python中最便宜的容器类型,它们也是可哈希的,因此可以用作字典键、集合成员等。


元组很便宜,但字符串的复制不便宜... - Paul Oyster
2
这也是Python中使用key参数进行排序的功能。 - user3850
1
这是一种7位思维模式,对于Unicode数据来说完全不合适。您必须使用完整的Unicode大小写折叠,或者根据Unicode排序算法使用主要排序强度。是的,这意味着无论哪种方式,都需要新复制字符串,但至少然后您可以进行二进制比较,而不必为每个代码点在表中搜寻。 - tchrist

0
import re
if re.match('tEXT', 'text', re.IGNORECASE):
    # is True

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