Python 重叠文本匹配:用零宽前瞻找出 ABABA 里的两个 ABA

前天 3阅读

需要数出现位置,而不是切出互不重叠的片段

在 ABABA 中,ABA 可以从位置零开始,也可以从位置二开始,两次出现共用了中间的 A。直接使用 re.finditer("ABA", text) 却只返回一次。这并非正则引擎漏看文本,而是普通搜索按不重叠的完整匹配向前推进,第一段已经占用了前三个字符。

如果任务是列出所有可能的起点,可以让完整模式只检查当前位置后面是否存在目标内容,不实际消耗它。这就是零宽前瞻适合的场景。下面使用 Python 3.12.14,保存代码为 demo.py 后执行 python demo.py,逐项比较起点、完整匹配与捕获组。

把文字放进前瞻内部的捕获组

模式 (?=(ABA)) 的外层是前瞻断言,内层括号记录被检查到的 ABA。每次成功时,完整匹配的起止位置相同,因为它没有消耗字符;捕获组仍然可以记下实际文字及其区间。finditer 会继续搜索后续位置,因此能够找到第二个起点。

代码还提供一个只接收非空字面目标的 overlapping 函数,用 re.escape 处理加号等正则特殊字符。它返回每次捕获的起点和文字,避免把用户给出的普通搜索词误当正则程序。空目标被主动拒绝,否则每个边界都可能成为一次命中,通常违背这个接口的用途。

Python 重叠文本匹配:用零宽前瞻找出 ABABA 里的两个 ABA

AI概念示意图:两条跨越相邻节点的弧线共享中间位置,表示两个允许重叠的出现起点。图片只说明概念,不是运行截图。

import re

text = "ABABA"
ordinary = [m.span() for m in re.finditer("ABA", text)]
assert ordinary == [(0, 3)]
print("ordinary:", ordinary)

matches = list(re.finditer(r"(?=(ABA))", text))
details = [(m.span(), m.group(0), m.span(1), m.group(1)) for m in matches]
assert details == [
    ((0, 0), "", (0, 3), "ABA"),
    ((2, 2), "", (2, 5), "ABA"),
]
assert re.fullmatch(r"(?=(ABA))", text) is None
print("lookahead:", details)

def overlapping(text, needle):
    if not needle:
        raise ValueError("needle must not be empty")
    pattern = re.compile("(?=(" + re.escape(needle) + "))")
    return [(m.start(1), m.group(1)) for m in pattern.finditer(text)]

found = overlapping("AAAA", "AA")
assert found == [(0, "AA"), (1, "AA"), (2, "AA")]
assert overlapping("+a+a", "+a") == [(0, "+a"), (2, "+a")]
assert overlapping("", "AA") == []
print("literal overlap:", found)
try:
    overlapping("ABC", "")
except ValueError as exc:
    assert str(exc) == "needle must not be empty"
else:
    raise AssertionError("empty needle must fail")
print("empty needle: rejected")

完整匹配为空,不等于没有匹配对象

ordinary 只有区间 (0, 3)。lookahead 则有两个记录:完整区间分别是 (0, 0)、(2, 2),完整匹配文字均为空串;捕获组区间却分别是 (0, 3)、(2, 5),内容都为 ABA。若输出时只读取 group(0),就会误以为所有有效结果都丢了。

这里的“完整匹配”指一次 Match 对象的零号分组,不是要求覆盖整条输入的 re.fullmatch 函数。示例专门断言 fullmatch 返回 None:零宽断言虽然能在起点检查到 ABA,却没有消费整个 ABABA。这两个名称接近的概念应分别理解。

literal overlap 输出 AAAA 中 AA 的三个起点零、一、二,确认相邻出现也能重叠。随后用 +a+a 检查字面加号被正确处理,空字符串输入则返回空列表。这些小样本让搜索约定比“看起来多匹配了一次”更明确。

不要把它当成所有子串的自动枚举器

同一位置如果内部表达式能匹配不同长度,前瞻仍会按该表达式的选择与贪婪规则产生一次成功结果,不会自动列出每一种长度组合。本文的辅助函数接受固定字面目标,避开了这个额外问题;复杂模式需要另写覆盖范围与预期结果。

重叠命中也不能直接当成可依次替换的互不冲突片段。例如两次 ABA 共享一个字符,若都要改写,先后顺序和替换范围就必须另定规则。这里仅验证搜索与位置,不提供重叠替换策略,也没有测试大规模文本性能。

对长文本和复杂正则,增加匹配起点可能增加计算量,应限制输入与模式复杂度。维护代码时,最好同时留下一个不重叠结果和一个重叠结果作对照,并明确业务究竟在数“出现起点”还是“可拆出的片段”,这样后续优化才不会改变统计含义。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。