Какой самый эффективный способ найти одну из нескольких подстрок в Python? - PullRequest
27 голосов
/ 09 мая 2009

У меня есть список возможных подстрок, например, ['кошка', 'рыба', 'собака']. На практике список содержит сотни записей.

Я обрабатываю строку, и мне нужно найти индекс первого появления любой из этих подстрок.

Для пояснения, для '012cat' результат равен 3, а для '0123dog789cat' - 4.

Мне также нужно знать, какая подстрока была найдена (например, ее индекс в списке подстрок или сам текст), или как минимум длина совпадающей подстроки.

Существуют очевидные способы грубой силы для достижения этого, я подумал, есть ли какое-нибудь элегантное решение Python / Regex для этого.

Спасибо, Ракс

Ответы [ 6 ]

32 голосов
/ 09 мая 2009

Я бы предположил, что регулярное выражение лучше, чем проверка каждой подстроки в отдельности, потому что концептуально регулярное выражение моделируется как 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

UPDATE: Следует соблюдать осторожность при объединении слов в единый образец альтернативных слов. Следующий код строит регулярное выражение, но экранирует любые специальные символы регулярного выражения и сортирует слова так, чтобы более длинные слова имели возможность совпадать с любыми более короткими префиксами того же слова:

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 реализует регулярные выражения, но чтобы ответить на вопрос Ракса о том, существуют ли ограничения 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)

ОБНОВЛЕНИЕ: Следует отметить, что порядок вещей ИЛИ, объединенных в регулярном выражении , имеет значение . Посмотрите на следующий тест, вдохновленный 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

Это говорит о том, что порядок имеет значение: - /. Я не уверен, что это значит для приложения Ракса, но, по крайней мере, поведение известно.

ОБНОВЛЕНИЕ: Я разместил этот вопрос о реализации регулярных выражений в Python , который, мы надеемся, даст нам некоторое представление о проблемах, обнаруженных с этим вопросом.

4 голосов
/ 09 мая 2009
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)
3 голосов
/ 10 мая 2009

Я просто хочу указать на разницу во времени между ответом 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

Я бы согласился с ответом Тома, как для удобства чтения, так и для скорости.

2 голосов
/ 09 мая 2009

Это неопределенный теоретический ответ без предоставленного кода, но я надеюсь, что он может указать вам верное направление.

Во-первых, вам потребуется более эффективный поиск списка подстрок. Я бы порекомендовал какую-то древовидную структуру. Начните с корня, затем добавьте узел 'a', если любая подстрока начинается с 'a', добавьте узел 'b', если любая подстрока начинается с 'b', и так далее. Для каждого из этих узлов продолжайте добавлять подузлы.

Например, если у вас есть подстрока со словом "муравей", у вас должен быть корневой узел, дочерний узел 'a', узел внука 'n' и узел правнука 't'.

Узлы должны быть достаточно легкими для создания.

class Node(object):
    children = []

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

, где name - символ.

Перебирайте строки по буквам. Следите за тем, на каком письме вы находитесь. На каждой букве попробуйте использовать следующие несколько букв, чтобы пройти по дереву. Если вы добились успеха, ваш номер буквы будет позицией подстроки, а в вашем порядке обхода будет указана найденная подстрока.

Уточняющее редактирование: DFA должны быть намного быстрее, чем этот метод, и поэтому я должен одобрить Том ответ . Я держу этот ответ только в том случае, если ваш список подстрок часто меняется, и в этом случае использование дерева может быть быстрее.

0 голосов
/ 09 мая 2009

Как насчет этого.

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

Очевидно, вы можете вернуть что-то, кроме кортежа.

Работает:

  • Фильтрация списка подстрок по тем, которые находятся в строке
  • Построение списка кортежей, содержащих индекс подстроки и подстроку
  • Если подстрока была найдена, найдите минимальное значение на основе индекса
0 голосов
/ 09 мая 2009

Прежде всего, я бы предложил вам отсортировать начальный список по возрастанию. Поскольку сканирование для более короткой подстроки происходит быстрее, чем сканирование для более длинной подстроки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...