【问题标题】:Python Regular Expressions to NFANFA 的 Python 正则表达式
【发布时间】:2019-03-16 03:29:02
【问题描述】:

我目前正在使用 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

有没有办法解决这个问题?这个想法是避免通过编译的正则表达式进行迭代以获得匹配。

【问题讨论】:

  • 你试过一个大的正则表达式吗?您可以创建命名捕获组并将它们与| 连接在一起。
  • 我会尝试一下,看看它是否能提高性能。如果我们谈论 NFA,我不确定我们是否可以模仿我们使用 re 捕获组的方式@
  • 我看到 David Beazley 做了一个可能对您有用的技巧。他组合了单独的正则表达式,将每个正则表达式放在一个命名组中,然后使用未记录的scanner 函数分别处理每个匹配项。见youtube.com/watch?v=D1twn9kLmYg。唯一的问题是您还使用命名组 - 也许您可以采用两步方法,首先匹配整体模式,然后将每个匹配项分解为您想要的部分。或者有一个从组到名称的单独映射。
  • 顺便说一句,您应该使用原始字符串来正确解释转义字符吗?例如。 r'^New York(?P&lt;grp1&gt;\d+/\d+): (?P&lt;grp2&gt;.+)$'
  • 是出现性能瓶颈的正则表达式的数量还是数据集的大小?

标签: python regex python-2.7


【解决方案1】:

您的任何正则表达式是否会破坏 DFA 兼容性?在您的示例中看起来不像。您可以在 C/C++ DFA 实现(如re2)周围使用Python wrapper,这是re 的替代品。如果正则表达式与 re2 syntax 不兼容,re2 也将回退到使用 re,因此它将优化所有可能的情况,并且不会在不兼容的情况下失败。

请注意,re2确实支持(?P&lt;name&gt;regex)捕获语法,但它支持(?P=&lt;name&gt;) backref sytnax。

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,而不是将它们存储在文件或内存中,并在它们处理完毕后将其移交给全部收集。

您还可以将所有正则表达式组合成一个交替组(r1)|(r2)|(r3)| ... |(rN) 的大 DFA 正则表达式,并在结果匹配对象上迭代您的组匹配,以尝试仅匹配相应的原始正则表达式。匹配结果对象将具有与 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

您还可以查看pyre,它是同一 C++ 库的更好维护的包装器,但不是 re 的替代品。还有一个Python Wrapper 代表RuRe,这是我所知道的最快的正则表达式引擎。

【讨论】:

    【解决方案2】:

    详细说明我的评论:将所有内容放在一个大的正则表达式中的问题是组名必须是唯一的。但是,您可以按如下方式处理您的正则表达式:

    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 就不必以任何方式更改他的代码来使用您的解决方案。剥离所有组名以确保您包装的组名是唯一的似乎是一个遥远的问题,但只要您可以重新创建匹配对象的潜在状态,它就足够简单了。
    • 我明白了。您需要删除所有组名,因为在将正则表达式连接在一起时组名会发生冲突。
    • 没错!在多个正则表达式中使用了 grp1、grp2、year 等,导致了问题。作为替代方案,您可以为组名添加前缀而不是去掉它们,有很多选项可以改进代码。
    【解决方案3】:

    我建议执行以下步骤:

    1. 创建一个名为 Patterns.csv 的 excel,其中包含两列 PatternsName 其中 pattern 是正则表达式模式,例如 ^New York(?P&lt;grp1&gt;\d+/\d+): (?P&lt;grp2&gt;.+)$',名称可以是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。那是一个可怕的 25mb 轮子。只需使用该死的import csv。天哪,废物。
    • @Julian:请阅读softwarerecs.stackexchange.com/questions/7463/…,了解将 csv 读取到 python 的最快方法是什么。在这里,OP 想要快速高效的东西
    • 看,pandas 从 CSV 解析数据的速度更快,但对于 20,000 行的数据集来说,它的速度要快 0.15 秒。由于这只是一次完成,我认为更大的浪费是你正在拉入的巨大图书馆。IE 矫枉过正。 OP 遇到的问题不是来自导入甚至循环遍历 REGEXES 列表,而是大型数据集比较问题。
    • 我们可以同意不同意,但我认为这不是正确的问题或空间
    【解决方案4】:

    我会看re.Scanner。它没有记录并标记为实验性的,但它是使用sre_parsesre_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)
    

    【讨论】:

      【解决方案5】:

      如果您的所有正则表达式模式都遵循相同的城市名称格式(未捕获),然后是捕获的一系列 /-delimited 数字、冒号和空格,然后是捕获的字符串的其余部分,您可以简单地用相同的正则表达式模式解析它们:

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

      【讨论】:

      • 这很痛苦,因为正则表达式是不同的模式:/
      • 你能用更多明显不同的正则表达式模式来更新你的问题吗?你举的两个例子很相似。
      • 我在上面的问题中添加了一些正则表达式
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-30
      • 2012-03-01
      • 2012-09-14
      • 2013-08-01
      • 2020-03-23
      • 1970-01-01
      相关资源
      最近更新 更多