在Python中找到几个子串之一的最有效方法是什么?

时间:2009-05-09 07:20:49

标签: python regex string substring

我有一个可能的子串列表,例如['猫','鱼','狗']。实际上,该列表包含数百个条目。

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

澄清一下,对于'012cat',结果为3,对于'0123dog789cat',结果为4.

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

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

谢谢, RAX

6 个答案:

答案 0 :(得分:32)

我认为正则表达式比单独检查每个子字符串要好,因为概念上正则表达式被建模为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)

END UPDATE

应该注意的是,您将希望尽可能少地形成正则表达式(即 - 调用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,希望能够让我们深入了解此问题所带来的问题。

答案 1 :(得分:4)

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)

答案 2 :(得分:3)

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

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

对于可读性和速度,我会选择汤姆的答案。

答案 3 :(得分:2)

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

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

例如,如果你有一个带有“ant”字样的子字符串,你应该有一个根节点,一个子节点'a',一个孙子节点'n'和一个伟大的孙子节点{{ 1}}。

节点应该很容易制作。

't'

其中class Node(object): children = [] def __init__(self, name): self.name = name 是一个字符。

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

澄清编辑:DFA应该比这种方法快得多,因此我应该赞同Tom's answer。我只是保留这个答案,以防你的子字符串列表经常更改,在这种情况下使用树可能更快。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

这个怎么样。

>>> 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')

显然,你可以返回一个元组以外的东西。

这适用于:

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