Python中不区分大小写的字符串startswith方法

67

这是我检查mystring是否以某个字符串开头的方法:

>>> mystring.lower().startswith("he")
True

问题在于mystring非常长(成千上万个字符),因此lower()操作需要很长时间。

问题:有更有效率的方法吗?

我的失败尝试:

>>> import re;
>>> mystring.startswith("he", re.I)
False

3
谢谢。我从你的“有问题”的例子中受益了。 - Jiminion
7个回答

66

您可以使用以下正则表达式:

In [33]: bool(re.match('he', 'Hello', re.I))
Out[33]: True 

In [34]: bool(re.match('el', 'Hello', re.I))
Out[34]: False 

对于一个包含2000个字符的字符串,这比lower()方法快了约20倍:

In [38]: s = 'A' * 2000

In [39]: %timeit s.lower().startswith('he')
10000 loops, best of 3: 41.3 us per loop

In [40]: %timeit bool(re.match('el', s, re.I))
100000 loops, best of 3: 2.06 us per loop

如果您需要反复匹配相同的前缀,预编译正则表达式可以带来很大的性能提升:

In [41]: p = re.compile('he', re.I)

In [42]: %timeit p.match(s)
1000000 loops, best of 3: 351 ns per loop

对于较短的前缀,先将前缀从字符串中切片,然后再将其转换为小写形式,可能会更快:

In [43]: %timeit s[:2].lower() == 'he'
1000000 loops, best of 3: 287 ns per loop

当然,这些方法的相对时间取决于前缀的长度。在我的机器上,折点似乎是六个字符左右,这时预编译的正则表达式成为最快的方法。

在我的实验中,逐个检查每个字符甚至可能更快:

In [44]: %timeit (s[0] == 'h' or s[0] == 'H') and (s[1] == 'e' or s[1] == 'E')
1000000 loops, best of 3: 189 ns per loop

然而,这种方法仅适用于在编写代码时已知的前缀,并且不适用于较长的前缀。


你的测试有点错误,因为你没有包括re.compile()的时间。 - Zaur Nasibov
3
关键在于“如果您重复匹配相同的前缀……”。这意味着编译的成本(约为900纳秒)会分摊到许多匹配中,并变得可以忽略不计。 - NPE
1
请注意,这仅适用于微不足道的情况,因为Python的正则表达式实现遗憾地不符合实际的Unicode标准。举一个例子,在不区分大小写的比较中,ß == SS应该为真,但是re.match('ß','SS',re.I)却不匹配。公平地说,lower()解决方案同样不正确,所以没有太大的伤害。 - Voo

35

这个怎么样:

prefix = 'he'
if myVeryLongStr[:len(prefix)].lower() == prefix.lower()

有趣的想法。 - Developer

10

另一个简单的解决方案是将元组传递给startswith(),以匹配所有需要匹配的情况,例如.startswith(('case1', 'case2', ..))

例如:

>>> 'Hello'.startswith(('He', 'HE'))
True
>>> 'HEllo'.startswith(('He', 'HE'))
True
>>>

2
在现实世界中,当两个字符串通常来自外部源时,这种情况不太可能发生...此外,生成所有选项非常低效(长度为n的字符串有2^n个选项)。 - The Godfather

4

如果你考虑到ASCII范围外的任何内容,那么给出的答案都不正确。

例如,在大小写不敏感的比较中,如果你遵循Unicode的大小写映射规则,则应将ß视为等于SS

为了获得正确的结果,最简单的解决方案是安装Python的regex模块,该模块遵循标准:

import re
import regex
# enable new improved engine instead of backwards compatible v0
regex.DEFAULT_VERSION = regex.VERSION1 

print(re.match('ß', 'SS', re.IGNORECASE)) # none
print(regex.match('ß', 'SS', regex.IGNORECASE)) # matches

1
关于您在德语语言 Stack 上评论“noob”的留言,我想告诉您没有“时效”,哈哈!它是一个雕像(statuTe)! - Buttle Butkus
@buttle Swype非常不同意这个观点。虽然我喜欢塑像的想法;-) - Voo

2

根据.lower()的性能,如果前缀足够小,多次检查相等可能会更快:

s =  'A' * 2000
prefix = 'he'
ch0 = s[0] 
ch1 = s[1]
substr = ch0 == 'h' or ch0 == 'H' and ch1 == 'e' or ch1 == 'E'

时间控制(使用与NPE相同的字符串):

>>> timeit.timeit("ch0 = s[0]; ch1 = s[1]; ch0 == 'h' or ch0 == 'H' and ch1 == 'e' or ch1 == 'E'", "s = 'A' * 2000")
0.2509511683747405

= 0.25 us per loop

与现有方法相比:

>>> timeit.timeit("s.lower().startswith('he')", "s = 'A' * 2000", number=10000)
0.6162763703208611

= 61.63 us per loop

(当然,这是非常糟糕的,但如果代码需要极高的性能,那么这样做可能是值得的)

如果那些毫秒计数很重要的话,使用Python本来就相当愚蠢。 - Mad Physicist

0
在Python 3.8中,最快的解决方案涉及切片和比较前缀,正如此答案所建议的那样:
def startswith(a_source: str, a_prefix: str) -> bool:
    source_prefix = a_source[:len(a_prefix)]
    return source_prefix.casefold() == a_prefix.casefold()

第二快的解决方案使用ctypes(例如,_wcsicmp)。注意:这是一个Windows示例。
import ctypes.util

libc_name = ctypes.util.find_library('msvcrt')
libc = ctypes.CDLL(libc_name)

libc._wcsicmp.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p)

def startswith(a_source: str, a_prefix: str) -> bool:
    source_prefix = a_source[:len(a_prefix)]
    return libc._wcsicmp(source_prefix, a_prefix) == 0

编译后的re解决方案是第三快的解决方案,包括编译成本。如果使用regex模块进行完整的Unicode支持,如此答案所建议的那样,该解决方案甚至会更慢。每个连续的匹配的成本与每个ctypes调用的成本大致相同。

lower()casefold()很昂贵,因为这些函数通过迭代源字符串中的每个字符(不考虑大小写)并相应地映射来创建新的Unicode字符串。 (请参见:内置函数str.lower()如何实现?)在该循环中花费的时间随着每个字符的增加而增加,因此,如果您处理的是短前缀和长字符串,请仅对前缀调用这些函数。


0

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