反向引用
使用反向引用再次匹配相同的文本
反向引用与捕获组先前匹配的文本匹配。假设我们要匹配一对开始和结束的HTML标记,以及两者之间的文本。通过将开始标记置于反向引用中,我们就可以将结束标记的标记名称重复使用。具体正则表达式如下:<([A-Z][A-Z0-9]*)\b[^>]*>.*?</\1>
。此正则表达式仅包含一个小括号的,其捕获的字符串是[A-Z][A-Z0-9]*
匹配到的。这是开始的HTML标记。(由于HTML标记不区分大小写,因此此正则表达式需要不区分大小写的匹配。)反向引用\1
(反斜杠后跟一个1)引用第一个捕获组。\1
匹配第一个捕获组所匹配的完全相同的文本。/
是一个文本字符。我们试图匹配的只是结束HTML标记中的正斜杠。
要找出特定反向引用的编号,请从左到右检测正则表达式。计算所有编号的捕获组的开头括号。第一个括号以反向引用号1开始,第二个是引用号2,以此类推。这里要忽略小括号的其他的语法功能(例如非捕获组),正则中的这些小括号是不计入编号的。这意味着非捕获括号还有另一个好处:我们可以将它们插入正则表达式中,而无需更改分配给反向引用的数字。这在修改复杂的正则表达式时非常有用。
我们可以多次重复使用相同的反向引用。([a-c])x\1x\1
匹配axaxa ,bxbxb和cxcxc 。
大多数正则表达式版本最多支持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
并不会使引擎前进到下一个字符,因为它的长度是零。正则表达式中的位置前进到[^>]
。
此步骤越过第一对捕获括号的右括号。这提示正则表达式引擎将它们内部匹配的内容存储到第一个反向引用中。在这种情况下,存储B。
存储反向引用后,引擎将继续进行匹配尝试。[^>]
与>不匹配。再次,因为有另一个星号*
,这也不是问题。字符串中的位置保持在> ,而正则表达式中的位置前进到>
。这显然可以匹配。下一个标记是一个.
,由一个星号*
重复。由于后面加了?
,所以是非贪婪的,正则表达式引擎最初会跳过此token,请注意,如果正则表达式的其余部分失败,它应该回退。
目前该引擎已到达正则表达式中的第二个<,和字符串总的第二个<匹配。匹配成功。下一个标记是/
。这与字符I不匹配,并且引擎被迫回溯到点号.
。点与字符串中的第二个<匹配。此时星号*
仍然是懒惰的,所以正则引擎再次注意到现有的回溯位置和token <
和I。这些不匹配,因此引擎再次回退。
回溯将继续,直到点号.
消耗了<I>bold italic
。此时,<
匹配字符串中的第三个< ,下一个标记是/
,它匹配/ 。下一个标记是\1
。请注意,该token是反向引用,而不是B。引擎不会在正则表达式中替换反向引用。引擎每次到达反向引用时,它都会读取存储的值。这意味着,如果引擎在第二次到达\1之前已回溯到第一对捕获括号之外,则将使用存储在第一个反向引用中的新值。但这并没有发生在这里,所以还是B。这在I处不匹配,因此引擎再次回溯,点.
消耗了字符串中的第三个< 。
回溯将继续,直到该点消耗了<I>bold italic</I>
为止。此时,<
匹配<并且/匹配/。引擎再次到达\1 。反向引用仍持有B。\1匹配B。正则表达式中的最后一个标记,> 匹配 > 。找到一个完整的匹配项:<B><I>bold italic</I><B>
。
回溯到捕获组
你可能想知道<([A-Z][A-Z0-9]*)\b[^>]*>.*?</\1>
中的字符边界\b
是起什么作用的。这是为了确保正则表达式不会匹配错误配对的标记,例如<BOO>bold</B>
。您可能会认为这不会发生,因为捕获组匹配BOO ,这会导致\1尝试匹配该匹配项并失败。确实是这样。但是随后正则表达式引擎是会回溯。
让我们使用正则表达式<([A-Z][A-Z0-9]*)[^>]*>.*?</\1>
(没有单词边界),并在regex引擎内部查看\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在O和O之间不匹配。这迫使[A-Z0-9]*
立即回溯。捕获组减少为B,并且B和O之间的单词边界失效。没有其他回溯位置,因此整个匹配尝试均失败。
我们需要单词边界的原因是我们正在使用[^>]*
跳过标签中的所有属性。如果您配对的标签从不具有任何属性,则可以将其排除在外,并使用<([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匹配失败。使用反向引用时,请始终仔细检查您是否确实在捕获所需的内容。
有用的示例:检查双字
编辑文本时,重复的单词如“the the”是经常容易出现并且很难被肉眼发现的,在文本编辑器中使用正则表达式\b (\w+)\s+\1\b
,你可以很容易地找到它们。要删除第二个单词,只需输入\1作为替换文本,然后单击“替换”按钮。
括号和反向引用不能在字符类中使用
括号不能在字符类中使用,至少不能用作元字符。当您将括号放在字符类中时,它将被视为文本字符。因此,正则表达式[(a)b]
与a,b,(和)匹配。
同样,反向引用也不能在字符类中使用。像(a)[\1b]
这样的正则表达式中的\1
要么是错误,要么是不必要的转义文字1。在JavaScript中,它是八进制转义。