如何使用 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
第三章内容
该模式支持以下四种章节组合:
| 文档包含的章节 | one | optional_two | optional_three |
|---|---|---|---|
| One、Two、Three | One 章节 | Two 章节 | Three 章节 |
| One、Two | One 章节 | Two 章节 | 未参与匹配 |
| One、Three | One 章节 | 未参与匹配 | Three 章节 |
| One | One 章节 | 未参与匹配 | 未参与匹配 |
未参与匹配的分组,在不同语言的 API 中可能表现为 null、空字符串,或不返回对应的捕获项,读取时需要按所用 API 的规则处理。
使用时需要注意什么?
开头的 (?msx) 启用了三个选项:
m:让^和$能匹配每一行的开头与结尾。s:让.也能匹配换行符。x:忽略模式中的排版空白,便于分行书写。
这里依靠 s 实现跨行匹配,用 m 识别独占一行的标题。原来的 [\w\W] 本身就能匹配换行,不需要 s。
\A 和 \z 分别限定文档开头和严格的文档末尾,避免表达式只匹配一个前缀就结束。这个模式要求文档直接从 One 开始;如果前面有说明文字,需要另外处理前导部分。
模式适用于 LF 或 CRLF 换行,标题后的 \r? 用于兼容 CRLF。标题必须占满整行,因此正文中的 Two examples 不会被识别为 Two 标题。如果标题允许前后有空格,需要相应调整标题规则。
.*? 中的 * 允许章节正文为空。如果正文中也可能出现独占一行的 Two 或 Three,仅凭当前文本格式就无法区分它们和真正的标题,需要增加标题标记、转义规则或其他结构信息。
备注:内容仅供参考。
