指南内含 1 场可玩狩猎

正则表达式反向引用:让正则记住自己匹配过的内容

像 \1 这样的反向引用,匹配的是某个分组已经捕获的那段文本。本文讲解它的原理,如何找出双写字母、重复单词和回文,以及它唯一不起作用的地方。

Regex Hunter 标志

· 阅读约 4 分钟

/(.)\1/

抓住

  • book
  • letter
  • coffee

放过

  • cat
  • dog
  • lemon

正则表达式的大部分内容描述的是文本的种类:一个数字、一个字母、任意一个字符。反向引用不一样,它说的是“我刚才匹配到的那段文本”。凭这一个概念,一个很短的正则就能找出双写字母、重复单词和回文,这些都是字符类描述不了的。

下面的每个示例都可以实时编辑。修改正则,或点击 试试,在沙盒中打开它。

01先捕获,再引用

括号有两个作用:分组和捕获。分组匹配到的文本会被保存下来,并按左括号从左到右编号。\1 表示“第 1 组捕获的文本”,\2 表示第 2 组,依此类推。

匹配6/6 符合预期
/(.)\1/

应当匹配3/3

  • book
  • letter
  • coffee

不应匹配3/3

  • cat
  • dog
  • lemon
在沙盒里试试
(.) 捕获一个字符,\1 要求同一个字符再出现一次。任何双写字母都能匹配。

关键在于相同的文本。(\w)\w 的意思是“一个字母,后面跟任意字母”,所以几乎什么都能匹配。(\w)\1 的意思是“一个字母,后面再跟一个完全相同的字母”。反向引用重复的不是分组里写的正则,而是它匹配出的结果。

02重复单词

校对时的一个经典错误是单词重复:“the the”。先捕获一个单词,再要求后面跟一个空格和同一个单词:

匹配4/4 符合预期
/\b(\w+) \1\b/

应当匹配2/2

  • this is the the end
  • it is is fine

不应匹配2/2

  • the then
  • that is it
在沙盒里试试
两端的 \b 把匹配限制在完整单词上,所以“the then”不会被当成两个“the”。

如果没有末尾的 \b,the then 就会匹配:the、一个空格,然后是 then 开头的 the。

03首尾字母相同

锚点和反向引用配合,可以描述一个单词的形状:

匹配6/6 符合预期
/^(.).*\1$/

应当匹配4/4

  • level
  • radar
  • stats
  • noon

不应匹配2/2

  • hello
  • regex
在沙盒里试试
第一个字母被捕获,.* 跳过中间部分,\1$ 要求最末尾是同一个字母。

04五个字母的回文

用两个分组,就能让单词形成镜像。捕获前两个字母,中间允许任意一个字母,再要求这两个字母按相反顺序出现:

匹配6/6 符合预期
/^(.)(.).\2\1$/

应当匹配4/4

  • level
  • radar
  • kayak
  • refer

不应匹配2/2

  • hello
  • lever
在沙盒里试试
第 1 组必须出现在最后,第 2 组必须出现在倒数第二位,所以单词正读反读都一样。

lever 很接近:它以 le 开头,但结尾是 er,不是 el。

05成对的引号

反向引用可以确保结束引号和开始引号一致。(["']) 捕获任意一种引号,\1 再用同一种引号收尾:

匹配2/2 符合预期
/(["']).*?\1/
  • He said "it's fine"
  • 'single' and "double"
在沙盒里试试
it's 里的撇号不会结束第一个匹配,因为这段字符串是用双引号开头的。

06命名组

在很长的正则里,数字编号很难看清。大多数引擎允许给分组命名,再按名字引用它:

正则14 字符
/(?<ch>.)\k<ch>/
在沙盒里试试
和 (.)\1 一样检查双写字母,只是分组命名为“ch”。

不同语言的语法不同。JavaScript、.NET 和 PCRE 使用 (?<name>…) 和 \k<name>。Python 内置的 re 模块使用 (?P<name>…) 和 (?P=name)。

07替换中的反向引用

替换文本时,捕获的分组同样有用。在大多数编辑器和编程语言中,你都可以把分组按新的顺序放回去:搜索 (\w+), (\w+),替换为 $2 $1(JavaScript、VS Code)或 \2 \1(Python、sed),就能把 Lovelace, Ada 变成 Ada Lovelace。

08唯一不起作用的地方:[ ] 内部

你可能很想写 (.)[^\1] 来表示“一个字符,后面跟一个不同的字符”。这样行不通。在字符类内部,\1 不是反向引用。JavaScript 会把它当成转义的字符编码,所以这个字符类的意思是“除编码为 1 的字符以外的任意字符”:

匹配2/2 符合预期
/(.)[^\1]/
  • aa
  • ab
在沙盒里试试
两行都能匹配。[^\1] 并不会和捕获到的字母做比较,所以“aa”也能通过。

要表示“不是同一个字符”,请使用负向先行断言:(.)(?!\1).。(详见我们的先行断言与后行断言指南。)

09试一试

在这块棋盘上,每个怪物都有两组不同的双写字母,比如 coffee 里的 ff 和 ee。每个无辜者都只有一组:

可玩狩猎标准杆 12

抓住含有两组双写字母的单词。

目标 · 抓住 0/5

  • coffee目标
  • balloon目标
  • bookkeeper目标
  • committee目标
  • mississippi目标

无辜词 · 误抓 0

  • book无辜
  • letter无辜
  • apple无辜
  • grass无辜
  • hello无辜

输入正则——棋盘会实时更新。

在沙盒里试试
喜欢吗?每天都有新的狩猎。玩今天的狩猎

10速查

  • ( … ) 用于捕获。分组按左括号编号。
  • \1、\2 … 匹配分组捕获的相同文本,而不是相同的正则。
  • (?<name>…) 搭配 \k<name>(JS、.NET、PCRE),或 (?P<name>…) 搭配 (?P=name)(Python)。
  • 替换时:$1(JavaScript)或 \1(Python、sed)。
  • [ ] 内的 \1 不是反向引用。

11继续练习