快速入门
教程
工具和语言
示例
参考
书籍评论
正则表达式教程
简介
目录
特殊字符
不可打印字符
正则表达式引擎内部结构
字符类
字符类减法
字符类交集
简写字符类
锚点
单词边界
交替
可选项
重复
分组和捕获
反向引用
反向引用,第 2 部分
命名组
相对反向引用
分支重置组
自由间距和注释
Unicode
模式修饰符
原子分组
占有量词
前瞻和后顾
环视,第 2 部分
将文本排除在匹配之外
条件
平衡组
递归
子例程
无限递归
递归和量词
递归和捕获
递归和反向引用
递归和回溯
POSIX 方括号表达式
零长度匹配
继续匹配
本网站上的更多内容
简介
正则表达式快速入门
正则表达式教程
替换字符串教程
应用程序和语言
正则表达式示例
正则表达式参考
替换字符串参考
书籍评论
可打印 PDF
关于本网站
RSS 源和博客
RegexBuddy—Better than a regular expression tutorial!

使用反向引用再次匹配相同文本

反向引用匹配先前通过捕获组匹配的相同文本。假设你要匹配一对 HTML 开始和结束标签,以及它们之间的文本。通过将开始标签放入反向引用中,我们可以为结束标签重复使用标签的名称。方法如下:<([A-Z][A-Z0-9]*)\b[^>]*>.*?</\1>。此正则表达式仅包含一对括号,捕获由 [A-Z][A-Z0-9]* 匹配的字符串。这是 HTML 开始标签。(由于 HTML 标签不区分大小写,此正则表达式需要不区分大小写的匹配。)反向引用 \1(反斜杠一)引用第一个捕获组。\1 匹配与第一个捕获组匹配的完全相同的文本。它之前的 / 是一个文本字符。它只是我们要尝试匹配的 HTML 结束标签中的正斜杠。

要找出特定反向引用的编号,请从左向右扫描正则表达式。计算所有编号捕获组的左括号。第一个括号开始反向引用编号一,第二个编号二,依此类推。跳过属于其他语法的括号,例如非捕获组。这意味着非捕获括号还有另一个好处:您可以在不更改分配给反向引用的编号的情况下将它们插入正则表达式中。在修改复杂正则表达式时,这可能非常有用。

您可以多次重复使用同一反向引用。 ([a-c])x\1x\1 匹配 axaxabxbxbcxcxc

大多数正则表达式风格最多支持 99 个捕获组和两位数反向引用。因此,如果您的正则表达式有 99 个捕获组,\99 是一个有效的反向引用。

查看正则表达式引擎内部

让我们看看正则表达式引擎如何将正则表达式 <([A-Z][A-Z0-9]*)\b[^>]*>.*?</\1> 应用于字符串 Testing <B><I>bold italic</I></B> text。正则表达式中的第一个标记是文本 <。正则表达式引擎遍历字符串,直到它可以在字符串中的第一个 < 处匹配。下一个标记是 [A-Z]。正则表达式引擎还注意到它现在位于第一对捕获括号内。 [A-Z] 匹配 B。引擎前进到 [A-Z0-9]>。此匹配失败。但是,由于 星号,这完全没问题。字符串中的位置仍保留在 >。因为 B 在前,所以 单词边界 \b> 处匹配。单词边界不会使引擎在字符串中前进。正则表达式中的位置前进到 [^>]

此步骤跨越第一对捕获括号的闭合括号。这会提示正则表达式引擎将与之匹配的内容存储在第一个反向引用中。在此情况下,存储了 B

存储反向引用后,引擎会继续尝试匹配。[^>] 不匹配 >。同样,由于另一个星号,这并不是问题。字符串中的位置停留在 >,正则表达式中的位置前进到 >。它们显然匹配。下一个标记是一个点,由一个惰性星号重复。由于惰性,正则表达式引擎最初跳过此标记,并注意如果正则表达式的其余部分失败,则应回溯。

引擎现在已到达正则表达式中的第二个 < 和字符串中的第二个 <。它们匹配。下一个标记是 /。这与 I 不匹配,引擎被迫回溯到点。该点匹配字符串中的第二个 <。星号仍然是惰性的,因此引擎再次注意可用的回溯位置,并前进到 <I。它们不匹配,因此引擎再次回溯。

回溯继续,直到点消耗了 <I>粗体斜体。此时,< 匹配字符串中的第三个 <,下一个标记是 /,它匹配 /。下一个标记是 \1。请注意,标记是反向引用,而不是 B。引擎不会在正则表达式中替换反向引用。每次引擎到达反向引用时,它都会读取已存储的值。这意味着如果引擎在第二次到达 \1 之前回溯到第一对捕获括号之外,则将使用存储在第一个反向引用中的新值。但这里没有发生这种情况,所以它就是 B。这无法在 I 处匹配,因此引擎再次回溯,并且该点消耗了字符串中的第三个 <

回溯继续进行,直到点消耗了 <I>粗体斜体</I>。此时,< 匹配 </ 匹配 /。引擎再次到达 \1。反向引用仍然保留 B\1 匹配 B。正则表达式中的最后一个标记 > 匹配 >。已找到一个完整匹配项:<B><I>粗体斜体</I></B>

回溯到捕获组

您可能对上述 <([A-Z][A-Z0-9]*)\b[^>]*>.*?</\1> 中的词边界 \b 感到好奇。这是为了确保正则表达式不会匹配成对不正确的标记,例如 <boo>粗体</b>。您可能认为不会发生这种情况,因为捕获组匹配 boo,这会导致 \1 尝试匹配相同的内容,然后失败。这确实会发生。但随后正则表达式引擎会回溯。

我们来取没有词边界的正则表达式 <([A-Z][A-Z0-9]*)[^>]*>.*?</\1>,并在 \1 第一次失败时查看正则表达式引擎内部。首先,.*? 继续扩展,直到达到字符串末尾,而 </\1>.*? 每次匹配一个更多字符时都会失败。

然后,正则表达式引擎回溯到捕获组。[A-Z0-9]* 已匹配 oo,但同样乐意匹配 o 或什么都不匹配。回溯时,[A-Z0-9]* 被迫放弃一个字符。正则表达式引擎继续,第二次退出捕获组。由于 [A-Z][A-Z0-9]* 现在已匹配 bo,因此将其存储到捕获组中,覆盖之前存储的 boo[^>]* 匹配开幕标签中的第二个 o>.*?</ 匹配 >bold<\1 再次失败。

正则表达式引擎再次进行所有相同的回溯,直到 [A-Z0-9]* 被迫放弃另一个字符,导致它不匹配任何内容,星号允许这样做。捕获组现在只存储 b[^>]* 现在匹配 oo>.*?</ 再次匹配 >bold<\1 现在成功,> 也是如此,并且找到了一个整体匹配。但不是我们想要的。

对此有多种解决方案。一种是使用单词边界。当 [A-Z0-9]* 第一次回溯时,将捕获组缩小到 bo\b 无法匹配 oo 之间。这迫使 [A-Z0-9]* 立即再次回溯。捕获组缩小到 b,单词边界在 bo 之间失败。没有进一步的回溯位置,因此整个匹配尝试失败。

我们需要单词边界的原因是,我们使用 [^>]* 跳过标记中的任何属性。如果您的配对标记没有任何属性,则可以将其省略,并使用 <([A-Z][A-Z0-9]*)>.*?</\1>。每次 [A-Z0-9]* 回溯时,其后的 > 无法匹配,从而快速结束匹配尝试。

如果您不希望正则表达式引擎回溯到捕获组,则可以使用原子组。原子分组教程部分包含所有详细信息。

重复和反向引用

正如我在上面的内部观察中提到的,正则表达式引擎不会永久替换正则表达式中的反向引用。它将在每次需要使用反向引用时使用保存到反向引用中的最后一个匹配项。如果通过捕获括号找到新的匹配项,则先前保存的匹配项将被覆盖。([abc]+)([abc])+ 之间存在明显差异。尽管两者都成功匹配 cab,但第一个正则表达式会将 cab 放入第一个反向引用中,而第二个正则表达式只会存储 b。这是因为在第二个正则表达式中,加号导致括号对重复三次。第一次,存储 c。第二次,a,第三次 b。每次都会覆盖前一个值,因此 b 保持不变。

这也意味着 ([abc]+)=\1 将匹配 cab=cab,而 ([abc])+=\1 则不会匹配。原因是当引擎到达 \1 时,它持有 b,而 c 无法匹配 b。在像这个一样简单的示例中,这是显而易见的,但它仍然是正则表达式中常见的一个困难原因。在使用反向引用时,务必仔细检查是否真的捕获到了想要的内容。

有用的示例:检查重复的单词

在编辑文本时,“the the” 等重复的单词很容易出现。在 文本编辑器 中使用正则表达式 \b(\w+)\s+\1\b,可以轻松地找到它们。要删除第二个单词,只需将 \1 作为替换文本输入,然后单击“替换”按钮即可。

不能在字符类中使用括号和反向引用

括号不能用在 字符类 中,至少不能用作元字符。当在字符类中放置括号时,它将被视为一个文本字符。因此,正则表达式 [(a)b] 匹配 ab()

反向引用也不能用在字符类中。在 (a)[\1b] 这样的正则表达式中,\1 要么是一个错误,要么是一个不必要的转义文本字符 1。在 JavaScript 中,它是一个 八进制转义