Какой самый быстрый способ найти в Python, соответствует ли строка каким-либо терминам в списке слов, фраз, логических AND? - PullRequest
5 голосов
/ 25 марта 2011

Я пытаюсь найти быстрый путь в Python, чтобы проверить, можно ли сопоставить список терминов со строками размером от 50 до 50 000 символов.

Термин может быть:

  • Слово, например. 'Яблоко'
  • Фраза, например. "вишневый пирог"
  • Логическое AND для слов и фраз, например. 'сладкий пирог и пикантный пирог и безе'

Совпадение - это то, где слово или фраза существуют вокруг границ слова, поэтому:

match(term='apple', string='An apple a day.') # True
match(term='berry pie', string='A delicious berry pie.') # True
match(term='berry pie', string='A delicious blueberry pie.') # False

У меня сейчас около 40 терминов, большинство из них простые слова. Количество терминов со временем будет увеличиваться, но я не ожидаю, что оно превысит 400.

Меня не интересует, какой термин (ы) соответствует строке или где в строке она совпадает, мне просто нужно значение true / false для соответствия каждой строке - гораздо более вероятно, что никакие термины не будут соответствует строке, поэтому для 1 из 500, где она совпадает, я могу сохранить строку для дальнейшей обработки.

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

Пока самое быстрое решение, которое я придумал, это:

def data():
    return [
        "The apple is the pomaceous fruit of the apple tree, species Malus domestica in the rose family (Rosaceae).",
        "This resulted in early armies adopting the style of hunter-foraging.",
        "Beef pie fillings are popular in Australia. Chicken pie fillings are too."
    ]

def boolean_and(terms):
    return '(%s)' % (''.join(['(?=.*\\b%s\\b)' % (term) for term in terms]))

def run():
    words_and_phrases = ['apple', 'cherry pie']
    booleans = [boolean_and(terms) for terms in [['sweet pie', 'savoury pie', 'meringue'], ['chicken pie', 'beef pie']]]
    regex = re.compile(r'(?i)(\b(%s)\b|%s)' % ('|'.join(words_and_phrases), '|'.join(booleans)))
    matched_data = list()
    for d in data():
        if regex.search(d):
            matched_data.append(d)

регулярное выражение в виде:

(?i)(\b(apple|cherry pie)\b|((?=.*\bsweet pie\b)(?=.*\bsavoury pie\b)(?=.*\bmeringue\b))|((?=.*\bchicken pie\b)(?=.*\bbeef pie\b)))

Таким образом, все термины объединяются в OR, регистр игнорируется, слова / фразы заключаются в \ b для границ слов, логические операторы AND используют предпросмотры, так что все термины совпадают, но они не должны совпадать в конкретный заказ.

Результат:

 print timeit.Timer('run()', 'from __main__ import run').timeit(number=10000)
 1.41534304619

Без предварительных просмотров (т. Е. Логических AND) это действительно быстро, но как только они добавлены, скорость значительно замедляется.

У кого-нибудь есть идеи, как это можно улучшить? Есть ли способ оптимизировать прогноз или, может быть, совершенно другой подход? Я не думаю, что остановка будет работать, поскольку она имеет тенденцию быть немного жадной к тому, что соответствует.

Ответы [ 2 ]

4 голосов
/ 25 марта 2011

Логическое И с регулярными выражениями с множественными утверждениями можно значительно ускорить, привязав их к началу строки. Или, что еще лучше, используйте два регулярных выражения: одно для списка OR ed с использованием метода re.search, а второе регулярное выражение с логическим списком AND ed с использованием метода re.match, например:

def boolean_and_new(terms):
    return ''.join([r'(?=.*?\b%s\b)' % (term) for term in terms])

def run_new():
    words_and_phrases = ['apple', 'cherry pie']
    booleans = [boolean_and_new(terms) for terms in [
        ['sweet pie', 'savoury pie', 'meringue'],
        ['chicken pie', 'beef pie']]]
    regex1 = re.compile(r'(?i)\b(?:%s)\b' % ('|'.join(words_and_phrases)))
    regex2 = re.compile(r'(?i)%s' % ('|'.join(booleans)))
    matched_data = list()
    for d in data():
        if regex1.search(d) or regex2.match(d):
            matched_data.append(d)

Эффективные регулярные выражения для этого набора данных:

regex1 = r'(?i)\b(?:apple|cherry pie)\b'
regex2 = r'(?i)(?=.*?\bsweet pie\b)(?=.*?\bsavoury pie\b)(?=.*?\bmeringue\b)|(?=.*?\bchicken pie\b)(?=.*?\bbeef pie\b)'

Обратите внимание, что второе регулярное выражение эффективно имеет привязку ^ в начале, так как оно используется с методом re.match. Это также включает несколько дополнительных (незначительных) настроек; удаление ненужных групп захвата и изменение жадной звездочки на ленивый. Это решение работает почти в 10 раз быстрее, чем оригинал на моей коробке Win32 с Python 3.0.1.

Дополнительно: Так почему же это быстрее? Давайте посмотрим на простой пример, который описывает, как работает «движок» NFA regex. (Обратите внимание, что следующее описание происходит от классической работы по этому вопросу: Освоение регулярных выражений (3-е издание) Джеффри Фридлом (MRE3), которая объясняет все эти вещи эффективности очень подробно - очень рекомендуется.) Допустим, у вас есть целевая строка, содержащая одно слово, и вы хотите, чтобы регулярное выражение проверяло, является ли это слово: "apple". Вот два регулярных выражения, которые можно составить для выполнения этой работы:

re1 = r'^apple'
re2 = r'apple'
s = r'orange'

Если ваша целевая строка: apple (или applesauce или apple-pie и т. Д.), То оба регулярных выражения будут успешно совпадать очень быстро. Но если ваша целевая строка говорит: orange, ситуация другая. Механизм регулярных выражений NFA должен попробовать все возможные перестановки регулярного выражения в целевой строке , прежде чем он сможет объявить ошибку совпадения. Способ, которым работает «движок» регулярных выражений, заключается в том, что он сохраняет внутренний указатель на свое текущее местоположение в целевой строке и второй указатель на местоположение в шаблоне регулярных выражений, а также продвигает эти указатели в процессе своей деятельности. Обратите внимание, что эти указатели указывают на местоположения между символами, и для начала указатель целевой строки устанавливается на местоположение перед первой буквой, если целевая строка.

re1: Первый токен в регулярном выражении - это ^ начало привязки строки. Этот «якорь» является одним из специальных выражений «утверждения», которое соответствует location в целевой строке и фактически не соответствует никаким символам. (Lookahead и lookbehind, а также выражения границы слова \b также являются утверждениями, которые соответствуют местоположению и не «потребляют» никаких символов.) Хорошо, указатель целевой строки инициализируется в месте перед первой буквой слова orange , механизм регулярных выражений проверяет, совпадает ли привязка ^, и это происходит (потому что это местоположение, фактически, является началом строки). Таким образом, указатель шаблона перемещается к следующему токену в регулярном выражении, букве a. (Указатель целевой строки не является расширенным). Затем он проверяет, соответствует ли литерал регулярного выражения a целевому строковому символу o. Это не соответствует. На этом этапе движок регулярных выражений достаточно умен, чтобы знать, что регулярные выражения никогда не преуспеют ни в каких других местах в пределах целевой строки (потому что ^ никогда не может совпадать нигде, кроме как в начале). Таким образом, он может объявить ошибку совпадения.

re2: В этом случае двигатель начинается с проверки, соответствует ли первый символ char a первому целевому символу 'o'.Еще раз, это не так.Однако в этом случае двигатель регулярных выражений не выполняется!Он определил, что шаблон не будет совпадать в первом местоположении, но он должен попытаться (и потерпеть неудачу) в ALL местоположениях с целевой строкой, прежде чем он сможет объявить ошибку совпадения.Таким образом, движок продвигает целевой указатель строки к следующему местоположению (Фридл называет это «передачей»).Для каждого «удара» он сбрасывает указатель шаблона обратно в начало.Таким образом, он проверяет первый токен в шаблоне a против второго символа в строке: r.Это также не совпадает, поэтому передача снова переходит к следующему месту в строке.В этот момент он проверяет первый символ шаблона a против третьего символа цели: a, что соответствует .Движок продвигает оба указателя и проверяет второй символ в регулярном выражении p против четвертого символа в цели n.Это не удается.В этот момент двигатель объявляет о неисправности в месте перед a в orange, а затем снова сталкивается с n.Это продолжается до тех пор, пока не произойдет сбой в каждом месте целевой строки, после чего он может объявить общий сбой соответствия.

Для длинных строк объекта эта дополнительная ненужная работа может занять много времени.Создание точного и эффективного регулярного выражения равнозначно искусству и науке.И чтобы создать действительно замечательное регулярное выражение, нужно точно понимать, как на самом деле работает двигатель под капотом.Получение этих знаний требует времени и усилий, но затраченное время (по моему опыту) окупится во много раз.И действительно, есть только одно хорошее место для эффективного обучения этим навыкам - сидеть и изучать Освоение регулярных выражений (3-е издание) , а затем практиковать изученные приемы.Я могу честно сказать, что это самая полезная книга, которую я когда-либо читал.(Это даже смешно! )

Надеюсь, это поможет!8 ^)

4 голосов
/ 25 марта 2011

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

...