使用Python正则表达式查找最后一个匹配

38

我想匹配字符串中最后一个简单模式,例如:

list = re.findall(r"\w+ AAAA \w+", "foo bar AAAA foo2 AAAA bar2")
print "last match: ", list[len(list)-1]

然而,如果字符串非常长,则会生成大量匹配项列表。有没有更直接的方法匹配第二个 "AAAA" 出现,或者我应该使用这个解决方法?


10
另一种选择是将字符串反转(mystr[::-1]),然后查找反向模式的第一个出现位置。 - ChristopheD
7
@ChristopheD,真恶心!唯一比正则表达式更难理解的是一个反向正则表达式。 - mlissner
目前的回答都没有解决“长字符串”的问题。>>> timeit.timeit(stmt = 'regex.search(long_string)', setup = "import re; regex=re.compile('b'); long_string='a'*int(10e8)+'b'; reverse_string=long_string[::-1]", number=10) 8.432429309999861 - Him
1
>>> timeit.timeit(stmt = 'regex.search(reverse_string)', setup = "import re; regex=re.compile('b'); long_string='a'*int(10e8)+'b'; reverse_string=long_string[::-1]", number=10)3.3803000405896455e-05 - Him
>>> timeit.timeit(stmt = 'regex.search(long_string)', setup = "import re; regex=re.compile('b$'); long_string='a'*int(10e8)+'b'; reverse_string=long_string[::-1]", number=10)7.993536103000224 - Him
显示剩余12条评论
5个回答

39

你可以使用表示行尾字符的$

>>> s = """foo bar AAAA
foo2 AAAA bar2"""
>>> re.findall(r"\w+ AAAA \w+$", s)
['foo2 AAAA bar2']

另外需要注意的是,list是一个不好的变量名,因为它会遮盖内置类型。要访问列表的最后一个元素,您可以使用[-1]索引:

>>> lst = [2, 3, 4]
>>> lst[-1]
4

1
如果我遇到了一个多行字符串,该怎么办? - SDD
3
对于这样的输入字符串:"foo bar AAAA foo2 AAAA bar2 bar3",这种方法行不通。当然,我们不知道是否可能出现这种情况,因为我们没有足够的信息。 - tzot
1
请注意,即使是 re.findall('abc', 'def') 也会返回一个空列表 [] 而不是 None - Jason Goal

34

你可以通过迭代所有匹配项并保留最后一个匹配项来避免构建列表:

def match_last(orig_string, re_prefix, re_suffix):

    # first use positive-lookahead for the regex suffix
    re_lookahead= re.compile(f"{re_prefix}(?={re_suffix})")

    match= None
    # then keep the last match
    for match in re_lookahead.finditer(orig_string):
        pass

    if match:
        # now we return the proper match

        # first compile the proper regex…
        re_complete= re.compile(re_prefix + re_suffix)

        # …because the known start offset of the last match
        # can be supplied to re_complete.match
        return re_complete.match(orig_string, match.start())

    return match

之后,match存储最后一次匹配的结果或者None
只要提供了可能重叠的正则表达式部分作为re_suffix,例如\w+,这对于所有patternsearched string的组合都适用。

>>> match_last(
    "foo bar AAAA foo2 AAAA bar2",
    r"\w+ AAAA ", r"\w+")
<re.Match object; span=(13, 27), match='foo2 AAAA bar2'>

2
这在很多用例中非常有用。通常您会想要替换模式的所有出现,然后自定义处理输入字符串的最后一部分。 - Rabih Kodeih

4

没有内置的re库功能支持从右到左的字符串解析,输入字符串仅从左到右搜索模式。

然而,有一个PyPi regex模块支持此功能。它是regex.REVERSE标志,或其内联变体(?r)

s="foo bar AAAA foo2 AAAA bar2"
print(regex.search(r"(?r)\w+ AAAA \w+$", s).group())
# => foo2 AAAA bar2

使用re模块,可以通过^[\s\S]*结构快速定位字符串结尾,并让回溯找到要捕获到单独组中的模式。然而,回溯可能会吞噬部分匹配(一旦所有后续模式匹配,它将停止产生更多文本),如果文本过大且没有匹配,则回溯可能变得灾难性。仅在输入字符串始终匹配或其长度较短且自定义模式不太依赖于回溯时才使用此技巧:
print(re.search(r"(?:^[\s\S]*\W)?(\w+ AAAA \w+)$", s).group(1))
# => foo2 AAAA bar2

这里的正则表达式 (?:^[\s\S]*\W)? 匹配一个可选序列,包括字符串开始、任意0个或多个字符和一个非单词字符 (\W)。添加 \W 是为了使回溯返回到非单词字符,它必须是可选的,因为匹配可能从字符串的开头开始。请参见Python演示

2
我不确定你原先的正则表达式是否能够给你想要的结果。如果我来晚了,那么很抱歉。但其他人也可能会觉得这个有用。
import re
p = r"AAAA(?=\s\w+)" #revised per comment from @Jerry
p2 =r"\w+ AAAA \w+"
s = "foo bar AAAA foo2 AAAA bar2"
l = re.findall(p, s)
l2 = re.findall(p2, s)
print('l: {l}'.format(l=l))

#print(f'l: {l}') is nicer, but online interpreters sometimes don't support it.
# https://www.onlinegdb.com/online_python_interpreter
#I'm using Python 3.

print('l2: {l}'.format(l=l2))
for m in re.finditer(p, s):
  print(m.span())
  #A span of (n,m) would really represent characters n to m-1 with zero based index
  #So.(8,12):
  # => (8,11: 0 based index)
  # => (9th to 12th characters conventional 1 based index)
print(re.findall(p, s)[-1])

输出:

l: ['AAAA', 'AAAA']
l2: ['bar AAAA foo2']
(8, 12)
(18, 22)   
AAAA

这里得到两个结果而不是一个的原因是“?=”这个特殊配方。
它被称为“正向先行断言”。在正则表达式评估期间,当找到匹配项时,它不会“消耗”(即推进光标)。因此,它在匹配后返回。
虽然正向先行断言在括号中,但它们也作为“非捕获组”起作用。
因此,尽管匹配了模式,但结果省略了由\ w +和示例中的介入空格\ s表示的周围字母数字字符序列以及表示[ \ t \ n \ r \ f \ v]的介入空格。 (更多here
所以每次我只得到AAAA。
这里的p2代表提问者@SDD的代码的原始模式。
"foo2"被该模式所消耗,因此第二个AAAA不会匹配,因为当正则表达式引擎在第二次迭代匹配时,光标已经向前移动太远。
我建议您查看Moondra在Youtube上的视频,如果您想更深入地了解。他已经制作了一系列非常详细的17个部分关于Python正则表达式的视频,从这里 开始。
这是一个指向在线 Python 解释器的链接

我不太明白为什么在 (?=\w+\s)AAAA(?=\s\w+) 中要使用 (?=\w+\s)?我觉得它实际上应该是一个正向回顾后发,即 (?<=\w+\s)AAAA(?=\s\w+)。此外,考虑到 OP 提到的长字符串,使用 re.findall() 可能会使您的解决方案变慢。 - Jerry
如果你尝试这样做,你会得到“后顾之忧需要固定宽度模式”的错误。请参见https://dev59.com/JmIj5IYBdhLWcg3wmmJE - JGFMK
正如我所指出的 - “不要推进光标” - 使用正向预查而不是正向回顾。 - JGFMK
1
好的,让我重新表述一下:在什么情况下,AAAA(?=\s\w+) 可能匹配到某些内容,而 (?=\w+\s)AAAA(?=\s\w+) 则会避免匹配到这些内容? - Jerry
首先,我写这篇文章已经有七八个月了。当时我刚刚审查了Moondras的视频,印象深刻。TFH: 我想我只是复制了原始海报的正则表达式。如果没有前导空格并且它位于字符串的开头,我相信您的正则表达式将返回AAAA,而我的则会忽略它。原始的OP正则表达式确实推进了光标,因此匹配数看起来少了一个。不推进光标是解决这个问题的方法。当您询问跨度字符范围时,它变得明显。 - JGFMK
显示剩余4条评论

0
另一种快速的方法是使用searchgroup
>>> re.search('\w+ AAAA \w+$',"foo bar AAAA foo2 AAAA bar2").group(0)
'foo2 AAAA bar2'

它是做什么的:

  1. 它使用模式\w+ AAAA \w+$,获取最后一个出现的与其相邻的单词和'AAAA'一起,并且所有都使用\w+(两次)和$(一次)。

  2. 在模式匹配过程之后,您将需要使用_sre.SRE_Match.group方法来获取_sre.SRE_Match对象的所属值,并当然获取第零组(第一组),因为search只保留一个匹配项(第零个)。

这里是regex101的链接。

以下是所有答案的时间表(不包括JGFMK的回答,因为它很困难):

>>> timeit.timeit(lambda: re.findall(r"\w+ AAAA \w+$", s),number=1000000) # SilentGhost
5.783595023876842
>>> timeit.timeit('import re\nfor match in re.finditer(r"\w+ AAAA \w+", "foo bar AAAA foo2 AAAA bar2"):pass',number=1000000) # tzot
5.329235373691631
>>> timeit.timeit(lambda: re.search('\w+ AAAA \w+$',"foo bar AAAA foo2 AAAA bar2").group(0),number=1000000) # mine (U9-Forward)
5.441731174121287
>>> 

我正在使用timeit模块测试所有的时间,同时我将number=1000000,这样会花费更长的时间。


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