如何使用正则表达式找到最短的重叠匹配?

Posted

技术标签:

【中文标题】如何使用正则表达式找到最短的重叠匹配?【英文标题】:How do I find the shortest overlapping match using regular expressions? 【发布时间】:2010-01-27 16:49:34 【问题描述】:

我对正则表达式还是比较陌生。我试图找到与特定模式匹配的最短文本字符串,但如果最短模式是更大匹配的子字符串,则会遇到麻烦。例如:

import re
string = "A|B|A|B|C|D|E|F|G"
my_pattern = 'a.*?b.*?c'

my_regex = re.compile(my_pattern, re.DOTALL|re.IGNORECASE)
matches = my_regex.findall(string)

for match in matches:
    print match

打印:

A|B|A|B|C

但我希望它返回:

A|B|C

有没有办法做到这一点,而不必遍历每个匹配项以查看它是否包含匹配的子字符串?

【问题讨论】:

请查看蒂姆的回答;这是最简洁的一个,可能应该标记为您的问题的答案。 【参考方案1】:

与此处的大多数其他答案相反,可以使用 positive lookahead assertion 和 capturing group 在单个正则表达式中完成:

>>> my_pattern = '(?=(a.*?b.*?c))'
>>> my_regex = re.compile(my_pattern, re.DOTALL|re.IGNORECASE)
>>> matches = my_regex.findall(string)
>>> print min(matches, key=len)
A|B|C

findall() 将返回所有可能的匹配项,因此您需要min() 才能获得最短的匹配项。

这是如何工作的:

我们不匹配此正则表达式中的任何文本,仅匹配字符串中的位置(正则表达式引擎在匹配尝试期间逐步执行)。 在每个位置,正则表达式引擎都会向前看,以查看您的正则表达式是否会在该位置匹配。 如果是这样,它将被捕获组捕获。 如果没有,就不会。 在任何一种情况下,正则表达式引擎都会前移一个字符并重复该过程,直到字符串结束。 由于前瞻断言不消耗任何字符,因此将找到所有重叠的匹配项。

【讨论】:

@JustinHarris:除非我们使用前瞻。 @TimPietzcker 好点,但我认为值得澄清这一点。我会删除我之前的评论。 @JustinHarris 你是对的。由于我不是母语人士,因此我非常愿意接受建议(例如,如何澄清我答案的最后一句话)。 @TimPietzcker 我主要关心的是这行:“findall() 将返回所有重叠匹配...”,我认为“findall() 在这种情况下将返回所有重叠匹配,因为前瞻。 ..”会更清楚。【参考方案2】:

没有。 Perl 返回最长、最左边的匹配项,同时遵守您的非贪婪量词。恐怕你必须循环播放。

编辑:是的,我意识到我在上面说过 Perl,但我相信 Python 确实如此。

【讨论】:

Perl?它与 Perl 有什么关系? 无赖。好的,这就是我的答案,但我想我会先与大师核实:)。谢谢。 无需循环。见my answer。 最左边的是,最长的不是。像 Perl 和 Python(search 模式)这样的正则表达式导向风格在最早可能的起始位置返回匹配,但不一定是在该位置的最长可能匹配。 Perl 正则表达式默认是贪婪的,所以最左边,最长的是 Perl。【参考方案3】:

这可能是sexegers 的有用应用。正则表达式匹配偏向于最长、最左边的选择。使用非贪婪量词(例如.*?)绕过最长的部分,并且反转输入和模式可以绕过最左匹配语义。

考虑以下根据需要输出A|B|C 的程序:

#! /usr/bin/env python

import re

string = "A|B|A|B|C|D|E|F|G"
my_pattern = 'c.*?b.*?a'

my_regex = re.compile(my_pattern, re.DOTALL|re.IGNORECASE)
matches = my_regex.findall(string[::-1])

for match in matches:
    print match[::-1]

另一种方法是制作更严格的模式。假设您不想允许重复出现的字符:

my_pattern = 'a[^a]*?b[^ab]*?c'

您的示例是通用且人为的,但如果我们对您正在使用的输入有更好的了解,我们可以提供更好、更有帮助的建议。

【讨论】:

所有反转所做的都是获得最右边匹配的语义,这同样被破坏,但对于不同的输入(例如“A|B|C|B|C”)。【参考方案4】:

另一个正则表达式解决方案;它只找到最后一次出现的 .*a.*b.*c:

my_pattern = 'a(?!.*a.*b.*c).*b[^c]*c'

a(?!.*a.*?b.*?c) 确保在第一个'A'之后没有'a.*?b.*?c' 结果中的 A|A|B|C 或 A|B|A|B|C 或 A|B|C|A|B|C 等字符串被消除

b[^c]*c 确保在“B”之后只有一个“C” 结果中的 A|B|C|B|C 或 A|B|C|C 等字符串被消除

所以你有最小的匹配'a.*?b.*?c'

【讨论】:

【参考方案5】:

正则表达式引擎从字符串的开头开始搜索,直到找到匹配项然后退出。因此,如果它甚至在考虑较小的匹配项之前就找到了匹配项,那么您无法强制它在同一运行中考虑以后的匹配项 - 您将不得不在子字符串上重新运行正则表达式。

设置全局标志并选择最短匹配字符串将无济于事,因为从您的示例中可以明显看出 - 较短的匹配可能是另一个匹配的子字符串(或部分包含在其中)。我相信您将不得不从(1 + 先前匹配的索引)开始后续搜索并继续进行。

【讨论】:

【参考方案6】:

我不认为这个任务可以通过一个正则表达式来完成。我没有证据证明是这种情况,但是有很多事情不能用正则表达式完成,我希望这个问题是其中之一。 this blog post 中给出了一些关于正则表达式限制的好例子。

【讨论】:

【参考方案7】:

您可以编写正则表达式,使其不能包含较小的匹配项。

对于您的正则表达式:

a.*?b.*?c

我想你可以这样写:

a[^ab]*b[^c]*c

要做到这一点很棘手,而且我没有看到任何更通用或更明显正确的方法来做到这一点。 (编辑——之前我提出了一个否定的前瞻断言,但我看不出有什么方法可以做到这一点。)

【讨论】:

【参考方案8】:

一个寻找最短匹配的 Python 循环,通过暴力测试从左到右的每个子字符串,选择最短的:

shortest = None
for i in range(len(string)):
    m = my_regex.match(string[i:])
    if m: 
        mstr = m.group()
        if shortest is None or len(mstr) < len(shortest):
            shortest = mstr

print shortest

另一个循环,这次让 re.findall 完成搜索所有可能匹配项的艰苦工作,然后从右到左暴力测试每个匹配项以寻找更短的子字符串:

# find all matches using findall
matches = my_regex.findall(string)

# for each match, try to match right-hand substrings
shortest = None
for m in matches:
    for i in range(-1,-len(m),-1):
        mstr = m[i:]        
        if my_regex.match(mstr):
            break
    else:
        mstr = m

    if shortest is None or len(mstr) < len(shortest):
        shortest = mstr

print shortest

【讨论】:

【参考方案9】:

不,Python 正则表达式引擎中没有。

不过,我对自定义函数的看法:

import re, itertools

# directly from itertools recipes
def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
    a, b = itertools.tee(iterable)
    for elem in b:
        break
    return itertools.izip(a, b)

def find_matches(rex, text):
    "Find all matches, even overlapping ones"
    matches= list(rex.finditer(text))

    # first produce typical matches
    for match in matches:
        yield match.group(0)

    # next, run it for any patterns included in matches
    for match1, match2 in pairwise(matches):
        subtext= text[match1.start()+1:match2.end()+1]
        for result in find_matches(rex, subtext):
            yield result

    # also test the last match, if there was at least one
    if matches:
        subtext= text[matches[-1].start()+1:matches[-1].end()+1]
        # perhaps the previous "matches[-1].end()+1" can be omitted
        for result in find_matches(rex, subtext):
            yield result

def shortest_match(rex, text):
    "Find the shortest match"
    return min(find_matches(rex, text), key=len)

if __name__ == "__main__":
    pattern= re.compile('a.*?b.*?c', re.I)
    searched_text= "A|B|A|B|C|D|E|F|G"
    print (shortest_match(pattern, searched_text))

【讨论】:

@TimPietzcker:感谢您的评论和回答。我从未尝试在前瞻或后视断言中捕获组。

以上是关于如何使用正则表达式找到最短的重叠匹配?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用正则表达式查找重叠匹配?

半小时学会正则表达式(上)

替换字符串中的重叠匹配项(正则表达式或字符串操作)

Python之字符串正则匹配

在 C# 中获取重叠的正则表达式匹配

如何检测两个正则表达式在它们可以匹配的字符串中是不是重叠?