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

18

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

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
请查看Tim的答案,它是最简洁的答案,可能应该标记为您问题的答案。 - tzot
9个回答

16

与大多数其他答案不同,在单个正则表达式中可以使用正向先行断言捕获组来完成此操作:

>>> 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()来获得最短的一个。

工作原理如下:

  • 在这个正则表达式中,我们没有匹配任何文本,只是字符串中的位置(在匹配尝试期间,正则表达式引擎会遍历这些位置)。
  • 在每个位置,正则表达式引擎都会向前查看,以确定您的正则表达式是否能够在该位置匹配。
  • 如果可以,它将被捕获到捕获组中。
  • 如果不能,则不会。
  • 无论哪种情况,正则表达式引擎都会向前移动一个字符并重复这个过程,直到字符串的末尾。
  • 由于前瞻断言不消耗任何字符,因此将找到所有重叠的匹配项。

1
@JustinHarris:除非我们在使用向前查看。 - Tim Pietzcker
@TimPietzcker 说得好,但我认为值得澄清一下。我会删除我之前的评论。 - Justin Harris
@JustinHarris 你说得对。由于我不是母语人士,我非常乐意听取建议(例如如何澄清我的回答的最后一句话)。 - Tim Pietzcker
1
@TimPietzcker 我的主要关注点是这句话:“findall()将返回所有重叠的匹配...”,我认为“在这种情况下,由于前瞻,findall()将返回所有重叠的匹配...”会更清晰明了。 - Justin Harris

1

另一种正则表达式解决方案;它仅查找.*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'


1
这可能是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”)来说是不同的。 - Jason Orendorff

1

不行。Perl 返回最长的、最左边的匹配,同时遵守你的非贪婪量词。恐怕你必须要循环。

编辑:是的,我意识到我之前说的是 Perl,但我相信 Python 也是这样。


Perl?它与Perl有什么关系? - SilentGhost
真遗憾。好吧,这就是我所预料的答案,不过还是想先向大师们确认一下 :). 谢谢。 - ryan
不需要循环。请参见我的答案 - Tim Pietzcker
1
最左匹配,不是最长匹配。像Perl和Python这样的正则表达式引擎(在“搜索”模式下)会在尽可能早的起始位置返回匹配项,但不一定是该位置上最长的匹配项。 - Alan Moore
Perl正则表达式默认是贪婪的,因此对于Perl来说,“从左到右,尽可能长”是正确的。 - Paul Beckingham
@Paul:这是完全错误的。当正则表达式(foo|foobar)应用于"foobar"时,它将匹配foo,因为第一个备选项匹配,所以下一个备选项甚至不会尝试。 - Tim Pietzcker

0

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

对于你的正则表达式:

a.*?b.*?c

我认为你可以写成这样:

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

这很棘手,我没有看到任何更一般或更明显正确的方法来做到这一点。(编辑-早先我建议使用负向先行断言,但我看不到让它起作用的方法。)


0
一个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

0

不,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:感谢您的评论和回答。我从未尝试过在前瞻或后顾断言中捕获组。 - tzot

0
正则表达式引擎从字符串的开头开始搜索,直到找到匹配项然后退出。因此,如果它在考虑较小的匹配项之前就找到了一个匹配项,那么您无法强制它在同一次运行中考虑后续匹配项 - 您必须在子字符串上重新运行正则表达式。
设置全局标志并选择最短匹配字符串也无济于事,正如您的示例所示 - 较短的匹配项可能是另一个匹配项的子字符串(或部分包含在其中)。我相信您将不得不从(上一个匹配项的索引+1)开始进行后续搜索,并像这样继续下去。

0
我不认为这个任务可以通过单个正则表达式完成。我没有证据证明这是事实,但有很多事情是无法用正则表达式来做的,而我就预料这个问题就是其中之一。一些关于正则表达式局限性的很好的例子可以在这篇博客文章中找到。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接