如何使用正则表达式找到最短的重叠匹配?
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:感谢您的评论和回答。我从未尝试在前瞻或后视断言中捕获组。以上是关于如何使用正则表达式找到最短的重叠匹配?的主要内容,如果未能解决你的问题,请参考以下文章