【问题标题】:What's the most efficient way to find one of several substrings in Python?在 Python 中查找多个子字符串之一的最有效方法是什么?
【发布时间】:2010-10-24 23:29:33
【问题描述】:

我有一个可能的子字符串列表,例如['cat', 'fish', 'dog']。实际上,该列表包含数百个条目。

我正在处理一个字符串,我正在寻找的是找到任何这些子字符串的第一次出现的索引。

澄清一下,'012cat' 的结果是 3,'0123dog789cat' 的结果是 4。

我还需要知道找到了哪个子字符串(例如,它在子字符串列表中的索引或文本本身),或者至少匹配的子字符串的长度。

有明显的蛮力方法来实现这一点,我想知道是否有任何优雅的 Python/正则表达式解决方案。

【问题讨论】:

  • 子字符串列表是否恒定?我问是因为使用正则表达式类型的解决方案通常涉及正则表达式的一些预计算(rsp。在您的情况下是子字符串列表)。该预计算是否会在多次搜索中摊销?

标签: python regex string substring


【解决方案1】:

我会假设正则表达式比单独检查每个子字符串更好,因为 从概念上讲正则表达式被建模为 DFA,因此当输入被消耗时,所有匹配项都在测试同时(导致对输入字符串进行一次扫描)。

所以,这里有一个例子:

import re

def work():
  to_find = re.compile("cat|fish|dog")
  search_str = "blah fish cat dog haha"
  match_obj = to_find.search(search_str)
  the_index = match_obj.start()  # produces 5, the index of fish
  which_word_matched = match_obj.group()  # "fish"
  # Note, if no match, match_obj is None

更新: 在将单词组合成一个替代单词的单一模式时应该小心。以下代码构建了一个正则表达式,但 escapes any regex special characters 并对单词进行排序,以便较长的单词有机会在相同单词的任何较短前缀之前匹配:

def wordlist_to_regex(words):
    escaped = map(re.escape, words)
    combined = '|'.join(sorted(escaped, key=len, reverse=True))
    return re.compile(combined)

>>> r.search('smash atomic particles').span()
(6, 10)
>>> r.search('visit usenet:comp.lang.python today').span()
(13, 29)
>>> r.search('a north\south division').span()
(2, 13)
>>> r.search('012cat').span()
(3, 6)
>>> r.search('0123dog789cat').span()
(4, 7)

结束更新

应该注意的是,您将希望尽可能少地形成正则表达式(即 - 调用 re.compile())。最好的情况是您提前知道您的搜索是什么(或者您计算一次/不经常),然后将 re.compile 的结果保存在某处。我的示例只是一个简单的废话函数,因此您可以看到正则表达式的用法。这里还有一些正则表达式文档:

http://docs.python.org/library/re.html

希望这会有所帮助。

更新:我不确定python如何实现正则表达式,但要回答Rax关于re.compile()是否有限制的问题(例如,您可以尝试将多少个单词“|”一次匹配),以及运行编译的时间量:这些似乎都不是问题。我尝试了这段代码,这足以说服我。 (我本可以通过添加时间和报告结果来做得更好,以及将单词列表放入一个集合中以确保没有重复......但这两项改进似乎都过大了)。这段代码基本上是即时运行的,并且让我相信我能够搜索 2000 个单词(大小为 10),并且它们中的一个将适当地匹配。代码如下:

import random
import re
import string
import sys

def main(args):
    words = []
    letters_and_digits = "%s%s" % (string.letters, string.digits)
    for i in range(2000):
        chars = []
        for j in range(10):
            chars.append(random.choice(letters_and_digits))
        words.append(("%s"*10) % tuple(chars))
    search_for = re.compile("|".join(words))
    first, middle, last = words[0], words[len(words) / 2], words[-1]
    search_string = "%s, %s, %s" % (last, middle, first)
    match_obj = search_for.search(search_string)
    if match_obj is None:
        print "Ahhhg"
        return
    index = match_obj.start()
    which = match_obj.group()
    if index != 0:
        print "ahhhg"
        return
    if words[-1] != which:
        print "ahhg"
        return

    print "success!!! Generated 2000 random words, compiled re, and was able to perform matches."

if __name__ == "__main__":
    main(sys.argv)

更新: 应该注意的是,在正则表达式中,事物的顺序 ORed 在一起很重要。看看以下受TZOTZIOY启发的测试:

>>> search_str = "01catdog"
>>> test1 = re.compile("cat|catdog")
>>> match1 = test1.search(search_str)
>>> match1.group()
'cat'
>>> match1.start()
2
>>> test2 = re.compile("catdog|cat")  # reverse order
>>> match2 = test2.search(search_str)
>>> match2.group()
'catdog'
>>> match2.start()
2

这表明顺序很重要:-/。我不确定这对 Rax 的应用程序意味着什么,但至少行为是已知的。

更新:我发布了this questions about the implementation of regular expressions in Python,希望能让我们深入了解这个问题中发现的问题。

【讨论】:

  • 这确实有效,但我有一个问题 - 正则表达式定义的大小没有限制吗?如果我有 1000 个子字符串,它还能工作吗?相对于单词的数量是否有任何显着的性能下降(即,这不仅仅是列表大小的线性)?关于您的其他说明,我的子字符串列表大约每天更新一次,我认为生成正则表达式定义并以此频率调用“编译”是没有问题的。非常感谢
  • @rax 你看到我的新解决方案了吗?我基本上修复了它的所有内容,并在此之后 20 秒提交。
  • @rax:希望我添加的示例代码能帮助您说服 re 模块会很好:-)。
  • 我也建议使用 "|".join(words),但是在 "this cacat is a cat" 中搜索 ["cacat", "cat"] 会返回 [(" cacat", 5), ("cat", 16)] 而不是 [("cacat", 5), ("cat", 7)],尽管这可能是想要的行为。
  • 许多所谓的正则表达式语法实际上并不是“正则”。也就是说,它们实际上比真正的正则表达式更强大,因此不能表示为 DFA。在 Python、Perl 甚至 grep 中出现的一个例子是反向引用。使用 Python 正则表达式 r"(a+)b\1"。这匹配了一些 a's、a b,然后是与以前相同数量的 a's。这是非常规的。支持反向引用的 RE 引擎实际上使用 NFA。一些 RE 引擎足够聪明,可以将 DFA 用于实际常规的正则表达式,但我认为 Python 不会这样做。
【解决方案2】:
subs = ['cat', 'fish', 'dog']
sentences = ['0123dog789cat']

import re

subs = re.compile("|".join(subs))
def search():
    for sentence in sentences:
        result = subs.search(sentence)
        if result != None:
            return (result.group(), result.span()[0])

# ('dog', 4)

【讨论】:

  • 我认为他只有1个“句子”
  • 谢谢,但这不是我要找的。首先,它没有找到第一次出现(在第二句中,它将返回“cat”的出现,即 10,而不是“dog”的出现,即 4)。有明显的解决方案,但它非常非常蛮力(迭代直到最后一个子字符串并不断保持第一次出现)。我的印象是 Python 必须为此提供一些库函数......
  • 我也不喜欢我的答案被“狙击”……但我并不是要抢你的风头。 +1,因为您的解决方案在技术上是正确的。两个 cmets:它没有讨论 Rax 的可扩展性问题,我不喜欢“return”语句,因为如果句子中有更多句子,它会过早退出。除此之外,它简短而中肯,值得一些声誉。
  • @Tom,“我不喜欢“return”语句,因为如果句子中有更多句子,它会过早退出。”但我以为 Rax 想找到第一场比赛?
  • @Unknown:我发表评论的原因是 if 您要在句子列表中添加更多句子,您的代码会短路因为它只会检查第一句话。即 - 如果您不打算编写泛化为更大列表的代码,您就不应该将列表用于子句和句子。
【解决方案3】:

我只想指出 DisplacedAussie 的回答和 Tom 的回答之间的时间差。两者都在使用一次时很快,所以你不应该有任何明显的等待,但是当你计时时:

import random
import re
import string

words = []
letters_and_digits = "%s%s" % (string.letters, string.digits)
for i in range(2000):
    chars = []
    for j in range(10):
        chars.append(random.choice(letters_and_digits))
    words.append(("%s"*10) % tuple(chars))
search_for = re.compile("|".join(words))
first, middle, last = words[0], words[len(words) / 2], words[-1]
search_string = "%s, %s, %s" % (last, middle, first)

def _search():
    match_obj = search_for.search(search_string)
    # Note, if no match, match_obj is None
    if match_obj is not None:
         return (match_obj.start(), match_obj.group())

def _map():
    search_for = search_for.pattern.split("|")
    found = map(lambda x: (search_string.index(x), x), filter(lambda x: x in search_string, search_for))
    if found:
        return min(found, key=lambda x: x[0])


if __name__ == '__main__':
    from timeit import Timer


    t = Timer("_search(search_for, search_string)", "from __main__ import _search, search_for, search_string")
    print _search(search_for, search_string)
    print t.timeit()

    t = Timer("_map(search_for, search_string)", "from __main__ import _map, search_for, search_string")
    print _map(search_for, search_string)
    print t.timeit()

输出:

(0, '841EzpjttV')
14.3660159111
(0, '841EzpjttV')
# I couldn't wait this long

为了可读性和速度,我会选择 Tom 的答案。

【讨论】:

  • 谢谢尼克!为了对 DisplacedAussie 公平起见,您可以通过删除对 split("|") 的调用来帮助他(一点点),然后给他一个开始的列表。为了更全面,您应该添加蛮力方法。 for word in search_for:, index = search_string.index(word), if index
  • +1 用于在效率问题上实际进行基准测试!
【解决方案4】:

这是一个模糊的理论答案,没有提供代码,但我希望它可以为您指明正确的方向。

首先,您需要更有效地查找子字符串列表。我会推荐某种树结构。从根开始,如果任何子字符串以'a' 开头,则添加'a' 节点,如果任何子字符串以'b' 开头,则添加'b' 节点,依此类推。对于这些节点中的每一个,继续添加子节点。

例如,如果你有一个包含单词“ant”的子字符串,你应该有一个根节点、一个子节点'a'、一个孙节点'n'和一个曾孙节点't'

节点应该很容易制作。

class Node(object):
    children = []

    def __init__(self, name):
        self.name = name

name 是一个字符。

逐个字母地遍历你的字符串。跟踪您所在的字母。在每个字母处,尝试使用接下来的几个字母来遍历树。如果你成功了,你的字母编号就是子串的位置,你的遍历顺序会指明找到的子串。

澄清编辑:DFA 应该比这种方法快得多,所以我应该支持Tom's answer。我只是在你的子字符串列表经常变化的情况下保持这个答案,在这种情况下使用树可能会更快。

【讨论】:

  • 谢谢,我完全理解字符串索引和搜索的理论和实践,并且可以自己实现它,但我希望 Python 有一个工具来处理这个确切的事情。我知道没有?
  • 我不知道 Python 内置了这样的功能,所以我不能说它是否存在。因此,恐怕这个答案至少对您没有帮助。我在这里看到的最接近的答案是汤姆的。
【解决方案5】:

首先,我建议您按升序对初始列表进行排序。因为扫描较短的子字符串比扫描较长的子字符串要快。

【讨论】:

  • 你确定这会有所作为吗?如果我自己(作为 DFA)实施正则表达式,长度无关紧要。将同时搜索每个子字符串。我现在很好奇python是如何实现正则表达式的......
【解决方案6】:

这个怎么样。

>>> substrings = ['cat', 'fish', 'dog']
>>> _string = '0123dog789cat'
>>> found = map(lambda x: (_string.index(x), x), filter(lambda x: x in _string, substrings))
[(10, 'cat'), (4, 'dog')]
>>> if found:
>>>     min(found, key=lambda x: x[0])
(4, 'dog')

显然,您可以返回元组以外的其他内容。

这是由:

  • 将子字符串列表过滤到字符串中的子字符串
  • 构建包含子字符串索引和子字符串的元组列表
  • 如果找到了子字符串,则根据索引找到最小值

【讨论】:

  • 这似乎是一个非常低效的答案。它肯定会多次扫描字符串。即使是您手动对要搜索的每个字符串使用字符串 index() 方法(动态跟踪最小值)的蛮力方法也比这更好。 map() 可以是一个强大的函数,但这不是这种情况的例子。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-07-14
  • 1970-01-01
  • 2010-11-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-23
相关资源
最近更新 更多