Регулярные выражения Python в NFA

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

Example:

REGEXES = [
    '^New York(?P<grp1>\d+/\d+): (?P<grp2>.+)$',
    '^Ohio (?P<grp1>\d+/\d+/\d+): (?P<grp2>.+)$',
    '(?P<year>\d{4}-\d{1,2}-\d{1,2})$',
    '^(?P<year>\d{1,2}/\d{1,2}/\d{2,4})$',
    '^(?P<title>.+?)[- ]+E(?P<epi>\d+)$'
    .
    .
    .
    .
]

Примечание: регулярные выражения не будут похожи

COMPILED_REGEXES = [re.compile(r, flags=re.I) for r in REGEXES]

def find_match(string):
    for regex in COMPILED_REGEXES:
        match = regex.search(string)
        if not match:
            continue
        return match

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

Вы пробовали вместо этого одно большое регулярное выражение? Вы можете создавать именованные группы захвата и объединять их вместе с |.

rumpelsepp 11.10.2018 08:25

Я попробую и посмотрю, улучшит ли он производительность. Если мы говорим о NFA, я не уверен, можем ли мы имитировать способ захвата групп с помощью re.

Abhi 11.10.2018 08:34

@rumpelsepp Я попробовал одно большое регулярное выражение, но столкнулся с несколькими проблемами с python re, поскольку он не поддерживает повторяющиеся имена групп и имеет ограничение на захват групп. В итоге я использовал другой модуль regex, у которого нет этих ограничений. Я не уверен, есть ли какая-то большая разница между тем, как работает re, и тем, как regex работает внутри.

Abhi 12.10.2018 13:46

можете ли вы предоставить образцы данных?

Pedro Rodrigues 17.10.2018 01:37

Я видел, как Дэвид Бизли проделал трюк, который может быть вам полезен. Он объединил отдельные регулярные выражения, поместив каждое в именованную группу, а затем использовал недокументированную функцию scanner для обработки каждого совпадения отдельно. См. youtube.com/watch?v=D1twn9kLmYg. Единственная проблема заключается в том, что вы также используете именованные группы - возможно, у вас может быть двухэтапный подход: сначала сопоставить общий образец, а затем разбить каждое сопоставление на нужные части. Или иметь отдельное сопоставление групп с именами.

EvertW 18.10.2018 15:55

Кстати, вы должны использовать необработанные строки для правильной интерпретации escape-символов? Например. r'^New York(?P<grp1>\d+/\d+): (?P<grp2>.+)$'

EvertW 18.10.2018 16:07

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

Julian 18.10.2018 22:27
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
9
7
2 959
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Если все ваши шаблоны регулярных выражений следуют одному и тому же формату названия города (не захвачено), за которым следует захваченная серия чисел с разделителями /, двоеточие и пробел, а затем захваченная остальная часть строки, вы можете просто проанализировать их все с тем же шаблоном регулярного выражения:

def find_match(string):
    return re.search(r'(?P<grp1>\d+(?:/\d+)*): (?P<grp2>.+)', string)

Это боль, потому что регулярные выражения - это другой шаблон: /

Abhi 11.10.2018 08:36

Можете ли вы обновить свой вопрос, добавив в него несколько совершенно разных шаблонов регулярных выражений? Два приведенных вами примера довольно похожи.

blhsing 11.10.2018 08:37

Я добавил еще несколько регулярных выражений в свой вопрос выше

Abhi 11.10.2018 08:46
Ответ принят как подходящий

Нарушает ли какое-либо из ваших регулярных выражений совместимость с DFA? Не похоже на ваши примеры. Вы можете использовать Обертка Python вокруг реализации DFA C / C++, такой как re2, которая является заменой re. re2 также вернется к использованию re, если регулярное выражение несовместимо с re2синтаксис, поэтому он оптимизирует все возможные случаи и не откажет в несовместимых случаях.

Обратите внимание, что re2делает поддерживает синтаксис захвата (?P<name>regex), но не поддерживает синтаксис обратной ссылки (?P=<name>).

try:
    import re2 as re
    re.set_fallback_notification(re.FALLBACK_WARNING)
except ImportError:
    # latest version was for Python 2.6
else:
    import re

Если у вас есть регулярные выражения с обратными ссылками, вы все равно можете использовать re2 с некоторыми особыми соображениями: вам нужно заменить обратные ссылки в своих регулярных выражениях на .*?, и вы можете найти ложные совпадения, которые вы можете отфильтровать с помощью re. В реальных данных ложные совпадения, вероятно, будут редкостью.

Вот наглядный пример:

import re
try:
    import re2
    re2.set_fallback_notification(re2.FALLBACK_WARNING)
except ImportError:
    # latest version was for Python 2.6

REGEXES = [
    '^New York(?P<grp1>\d+/\d+): (?P<grp2>.+)$',
    '^Ohio (?P<grp1>\d+/\d+/\d+): (?P<grp2>.+)$',
    '(?P<year>\d{4}-\d{1,2}-\d{1,2})$',
    '^(?P<year>\d{1,2}/\d{1,2}/\d{2,4})$',
    '^(?P<title>.+?)[- ]+E(?P<epi>\d+)$',
]

COMPILED_REGEXES = [re.compile(r, flags=re.I) for r in REGEXES]
# replace all backrefs with .*? for re2 compatibility
# is there other unsupported syntax in REGEXES?
COMPILED_REGEXES_DFA = [re2.compile(re2.sub(r'\\d|\\g\\d|\\g\<\d+\>|\\g\<\w+\>', '.*?', r), flags=re2.I) for r in REGEXES]

def find_match(string):
    for regex, regex_dfa in zip(COMPILED_REGEXES, COMPILED_REGEXES_DFA):
        match_dfa = regex_dfa.search(string)
        if not match_dfa:
            continue
        match = regex.search(string)
        # most likely branch comes first for better branch prediction
        if match:
            return match

Если этого недостаточно, вы можете использовать различные методы для передачи попаданий DFA в re по мере их обработки, вместо того, чтобы хранить их в файле или в памяти и передавать их после того, как они все собраны.

Вы также можете объединить все свои регулярные выражения в одно большое регулярное выражение DFA из чередующихся групп (r1)|(r2)|(r3)| ... |(rN) и перебирать совпадения групп в результирующем объекте совпадения, чтобы попытаться сопоставить только соответствующие исходные регулярные выражения. Объект результата совпадения будет иметь то же состояние, что и исходное решение OP.

# rename group names in regexeps to avoid name collisions
REGEXES_PREFIXED = [re2.sub(r'\(\?P\<(\w+)\>', r'(P<re{}_\1>'.format(idx), r) for idx, r in enumerate(REGEXES)]
# wrap and fold regexps (?P<hit0>pattern)| ... |(?P<hitN>pattern)
REGEX_BIG = ''
for idx, r in enumerate(REGEXES_PREFIXED):
    REGEX_BIG += '(?P<hit{}>{})|'.format(idx, r)
else:
    REGEX_BIG = REGEX_BIG[0:-1]
regex_dfa_big = re2.compile(REGEX_BIG, flags = re2.I)

def find_match(string):
    match_dfa = regex_dfa_big.search(string)
    if match_dfa:
        # only interested in hit# match groups
        hits = [n for n, _ in match_dfa.groupdict().iteritems() if re2.match(r'hit\d+', n)]
        # check for false positives
        for idx in [int(h.replace('hit', '')) for h in hits]
            match = COMPILED_REGEXES[idx].search(string)
            if match:
                return match

Вы также можете взглянуть на костер, который является лучше поддерживаемой оболочкой для той же библиотеки C++, но не заменяет re. Также есть Python-оболочка для RuRe, который является самым быстрым движком регулярных выражений, о котором я знаю.

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

import re

REGEXES = [
    r'^New York(?P<grp1>\d+/\d+): (?P<grp2>.+)$',
    r'^Ohio (?P<grp1>\d+/\d+/\d+): (?P<grp2>.+)$',
    r'(?P<year>\d{4}-\d{1,2}-\d{1,2})$',
    r'^(?P<year>\d{1,2}/\d{1,2}/\d{2,4})$',
    r'^(?P<title>.+?)[- ]+E(?P<epi>\d+)$']

# Find the names of groups in the regexps
groupnames = {'RE_%s'%i:re.findall(r'\(\?P<([^>]+)>', r) for i, r in enumerate(REGEXES)}

# Convert the named groups into unnamed ones
re_list_cleaned = [re.sub(r'\?P<([^>]+)>', '', r) for r in REGEXES]

# Wrap each regexp in a named group
token_re_list = ['(?P<RE_%s>%s)'%(i, r) for i, r in enumerate(re_list_cleaned)]

# Put them all together
mighty_re = re.compile('|'.join(token_re_list), re.MULTILINE)

# Use the regexp to process a big file
with open('bigfile.txt') as f:
    txt = f.read()
for match in mighty_re.finditer(txt):
    # Now find out which regexp made the match and put the matched data in a dictionary
    re_name = match.lastgroup
    groups = [g for g in match.groups() if g is not None]
    gn = groupnames[re_name]
    matchdict = dict(zip(gn, groups[1:]))
    print ('Found:', re_name, matchdict)

Я читал ваш код внимательнее. Это немного неортодоксально, но имеет смысл. Небольшая проблема заключается в том, что вы аннулировали match.groupdict (), и OP может использовать это в своем коде в другом месте. Я понимаю, что matchdict идентичен. Возможно, вы можете обновить внутреннюю структуру match, чтобы OP не изменял свой код каким-либо образом, чтобы использовать ваше решение. Кажется отдаленной задачей удалить все имена групп, чтобы убедиться, что те, которые вы обертываете, уникальны, но это достаточно просто, если вы можете воссоздать предполагаемое состояние объекта сопоставления.

okovko 18.10.2018 19:02

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

okovko 18.10.2018 20:49

Точно! grp1, grp2, year и т. д. использовались в нескольких регулярных выражениях, вызывая проблемы. В качестве альтернативы вы можете добавить префикс к именам групп, а не удалять их, есть много вариантов для улучшения кода.

EvertW 19.10.2018 10:46

Предлагаю сделать следующие шаги:

  1. Создайте файл Excel с именем Patterns.csv и добавьте в него два столбца Узоры и Имя, где шаблон - это шаблон регулярного выражения, такой как ^New York(?P<grp1>\d+/\d+): (?P<grp2>.+)$', а имя может быть New York. Это поможет вам сохранить все регулярные выражения на отдельном ресурсе, отличном от вашего кода. Это поможет вам в будущем, если вы захотите добавлять / вычитать / изменять регулярные выражения.

  2. Прочтите этот csv, используя команду ниже:

    import pandas as pd
    df = pd.read_csv("\\Patterns.csv")

  3. Напишите код для анализа этого CSV, как показано ниже:

    pattern = df['pattern'].tolist() pattern_name = df['name'].tolist() pattern_dict = dict(zip(pattern_name, pattern))

  4. Напишите шаблон регулярного выражения, чтобы узнать все совпадающие значения:

import collections sep = " ;; " NLU_Dict=collections.defaultdict() for pn, p in pattern_dict.items(): val = sep.join([sep.join(filter(lambda x: len(str(x).strip()) >0, map(str, v))) for in re.findall(p, text, re.I)]) NLU_Dict[pn] = val

Ваш NLU_Dict будет диктовкой. разделены ;;, содержащие значения имен шаблонов, которые совпадают, и пустые для того, что не соответствует.

ПОЖАЛУЙСТА, не используйте pandas для чтения в CSV. Это офигенный 25-мегабайтный руль. Просто используйте проклятый import csv. Боже мой, отходы.

Julian 18.10.2018 22:25

@Julian: Прочтите softwarerecs.stackexchange.com/questions/7463/…, чтобы узнать, каков самый быстрый способ чтения csv в python. И здесь OP хочет что-то быстрое и эффективное.

Rahul Agarwal 19.10.2018 08:42

Посмотрите, pandas быстрее анализирует данные из CSV, но это примерно на 0,15 секунды быстрее для набора данных из 20000 строк. Так как это делается только ОДИН РАЗ, я бы сказал, что большая потеря - это огромная библиотека, которую вы втягиваете. IE излишек. Проблемы, с которыми сталкивается OP, не связаны с импортом или даже циклом по списку REGEXES, проблема заключается в сравнении больших наборов данных.

Julian 19.10.2018 15:46

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

Rahul Agarwal 19.10.2018 16:38

Я бы посмотрел на re.Scanner. Он недокументирован и помечен как экспериментальный, но является хорошим примером использования sre_parse и sre_compile для построения регулярного выражения путем синтаксического анализа, слияния, а затем компиляции. Если вам не нужны имена групп, и вы хотите только захватывать группы, это должно сработать. Имейте в виду, что в этом коде нет проверки ошибок.

import re
import sre_parse
import sre_compile


def compile_multiple(subpatterns, flags=0):
    """
    Return a compiled regex from an iterable collection of
    pattern strings so that it matches any of the patterns
    in the collection.
    """
    from sre_constants import BRANCH, SUBPATTERN
    if isinstance(flags, re.RegexFlag):
        flags = flags.value
    pattern = sre_parse.Pattern()
    pattern.flags = flags
    parsed_subpatterns = []
    for subpattern in subpatterns:
        gid = pattern.opengroup()
        parsed_subpattern = sre_parse.parse(subpattern, flags)
        parsed_subpatterns.append(sre_parse.SubPattern(pattern, [
            (SUBPATTERN, (gid, 0, 0, sre_parse.parse(subpattern, flags))),
        ]))
        pattern.closegroup(gid, parsed_subpatterns[-1])
    combined_pattern = sre_parse.SubPattern(pattern, [(BRANCH, (None, parsed_subpatterns))])
    return sre_compile.compile(combined_pattern)

Другие вопросы по теме