PAYATHON 2026

如何使用 regex 进行多行匹配,并让后续部分可选且不被前面的部分吞并?

技术老齐

使用非贪婪匹配,并为每个章节指定结束边界:One 遇到 Two、Three 或文档末尾时停止,Two 遇到 Three 或文档末尾时停止,再把后两个分组设为可选。

如果章节顺序固定为 One → Two → Three,标题各占一行且没有前后空格,可以使用以下 PCRE 模式:

(?msx)
\A
(?<one>
    One\r?$ .*?
    (?= ^(?:Two|Three)\r?$ | \z )
)
(?<optional_two>
    ^Two\r?$ .*?
    (?= ^Three\r?$ | \z )
)?
(?<optional_three>
    ^Three\r?$ .*
)?
\z

每个命名分组都会捕获对应章节的标题和正文,包括换行符。

为什么加上 ? 后,前面的分组会吞掉后续章节?

原模式中的 [\w\W]+ 是贪婪匹配,会尽量匹配更多字符,包括换行符。当 Two 和 Three 都是必需分组时,引擎需要通过回溯给后面的分组留出内容,才能让整个表达式匹配成功。

把后两个分组改成可选后:

(?<one>One[\w\W]+)(?<optional_two>Two[\w\W]+)?(?<optional_three>Three[\w\W]+)?

第一个分组可以一直匹配到文档末尾。即使跳过后两个分组,整个表达式也能匹配成功,因此引擎不必回溯,把后续章节的内容留给它们。

只把 + 改成非贪婪的 +? 也不够。后面的分组可以跳过,如果没有结束约束,表达式可能只匹配文档开头的一小段就结束。

如何限制每个章节的范围?

one 的结束条件是:

(?= ^(?:Two|Three)\r?$ | \z )

这是正向先行断言,检查当前位置是否满足以下任一条件:

  • 后面是独占一行的 Two 或 Three 标题。
  • 已经到达文档末尾。

先行断言不消耗字符。因此,One 在 Two 标题前停止后,optional_two 仍然可以从这个标题开始匹配。

配合 .*?,one 会在第一个满足条件的位置停止。即使没有 Two,它也能在 Three 前停止。

optional_two 的结束条件只需检查 Three 或文档末尾:

(?= ^Three\r?$ | \z )

两个分组外面的 ? 表示整个章节可以不存在:

(?<optional_two>...)?
(?<optional_three>...)?

匹配示例

假设文档中缺少 Two:

One
第一章内容
Three
第三章内容

捕获结果为:

one:
One
第一章内容

optional_two:
未参与匹配

optional_three:
Three
第三章内容

该模式支持以下四种章节组合:

文档包含的章节oneoptional_twooptional_three
One、Two、ThreeOne 章节Two 章节Three 章节
One、TwoOne 章节Two 章节未参与匹配
One、ThreeOne 章节未参与匹配Three 章节
OneOne 章节未参与匹配未参与匹配

未参与匹配的分组,在不同语言的 API 中可能表现为 null、空字符串,或不返回对应的捕获项,读取时需要按所用 API 的规则处理。

使用时需要注意什么?

开头的 (?msx) 启用了三个选项:

  • m:让 ^ 和 $ 能匹配每一行的开头与结尾。
  • s:让 . 也能匹配换行符。
  • x:忽略模式中的排版空白,便于分行书写。

这里依靠 s 实现跨行匹配,用 m 识别独占一行的标题。原来的 [\w\W] 本身就能匹配换行,不需要 s。

\A 和 \z 分别限定文档开头和严格的文档末尾,避免表达式只匹配一个前缀就结束。这个模式要求文档直接从 One 开始;如果前面有说明文字,需要另外处理前导部分。

模式适用于 LF 或 CRLF 换行,标题后的 \r? 用于兼容 CRLF。标题必须占满整行,因此正文中的 Two examples 不会被识别为 Two 标题。如果标题允许前后有空格,需要相应调整标题规则。

.*? 中的 * 允许章节正文为空。如果正文中也可能出现独占一行的 Two 或 Three,仅凭当前文本格式就无法区分它们和真正的标题,需要增加标题标记、转义规则或其他结构信息。

备注:内容仅供参考。