快速入门
教程
工具和语言
示例
参考
书评
示例
正则表达式示例
数字范围
浮点数
电子邮件地址
IP 地址
有效日期
数字日期转文本
信用卡号
匹配整行
删除重复行
编程
两个相邻的单词
陷阱
灾难性回溯
重复次数过多
拒绝服务
使所有内容可选
重复捕获组
混合使用 Unicode 和 8 位
本网站上的更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式示例
正则表达式参考
替换字符串参考
书评
可打印 PDF
关于本网站
RSS 源和博客
RegexBuddy—The best regular expression debugger!

重复捕获组与捕获重复组

在创建需要捕获组来获取匹配文本部分的正则表达式时,一个常见的错误是重复捕获组,而不是捕获重复组。区别在于,重复捕获组只会捕获最后一次迭代,而捕获重复组的组将捕获所有迭代。一个示例将阐明这一点。

假设您想匹配类似 !abc!!123! 的标签。只有这两种情况是可能的,您想捕获 abc123 来确定您获得了哪个标签。这很容易:!(abc|123)! 可以解决问题。

现在,假设该标签可以包含多个 abc123 序列,例如 !abc123!!123abcabc!。快速简单的解决方案是 !(abc|123)+!。此正则表达式确实会匹配这些标签。然而,它不再满足将标签的标签捕获到捕获组中的要求。当此正则表达式匹配 !abc123! 时,捕获组仅存储 123。当它匹配 !123abcabc! 时,它仅存储 abc

如果我们看看正则表达式引擎如何将 !(abc|123)+! 应用于 !abc123!,这一点很容易理解。首先,! 匹配 !。然后,引擎进入捕获组。它注意到在引擎到达主题字符串中第一个和第二个字符之间的位置时,捕获组 #1 已进入。组中的第一个标记是 abc,它匹配 abc。找到了匹配项,因此不会尝试第二个备选方案。(引擎确实存储了一个回溯位置,但此示例中不会使用它。)现在,引擎离开捕获组。它注意到在引擎到达字符串中第 4 个和第 5 个字符之间的位置时,捕获组 #1 已退出。

在退出组后,引擎会注意到加号。加号是贪婪的,因此会再次尝试该组。引擎再次进入组,并注意到在字符串中第 4 个和第 5 个字符之间输入了捕获组 #1。它还注意到,由于加号不是独占的,因此可能会回溯。也就是说,如果无法再次匹配该组,那也没关系。在此回溯注释中,正则表达式引擎还会保存组在组的先前迭代期间的入口和出口位置。

abc 无法匹配 123,但 123 匹配成功。该组再次退出。存储了字符 7 和 8 之间的退出位置。

加号允许进行另一次迭代,因此引擎再次尝试。存储了回溯信息,并保存了该组的新入口位置。但现在,abc123 都无法匹配 !。该组失败,引擎回溯。在回溯时,引擎恢复了该组的捕获位置。即,该组在字符 4 和 5 之间进入,在字符 7 和 8 之间退出。

引擎继续使用 !,它匹配 !。找到了一个整体匹配项。整体匹配项跨越整个主题字符串。捕获组捕获字符 5、6 和 7,或 123。找到匹配项后,将丢弃回溯信息,因此无法在事后得知该组之前有一个匹配 abc 的迭代。(唯一的例外是 .NET 正则表达式引擎,它确实会在匹配尝试后为捕获组保留回溯信息。)

现在,捕获此示例中的 abc123 的解决方案应该很明显了:正则表达式引擎应该只进入和离开该组一次。这意味着加号应该在捕获组内部,而不是外部。由于我们需要对这两个备选方案进行分组,因此我们需要在重复组周围放置第二个捕获组:!((abc|123)+)!。当此正则表达式匹配 !abc123! 时,捕获组 #1 将存储 abc123,组 #2 将存储 123。由于我们对内部组的匹配不感兴趣,因此我们可以通过使内部组为非捕获组来优化此正则表达式:!((?:abc|123)+)!