指南内含 1 场可玩狩猎

正则表达式中的命名组与非捕获组

括号有两项工作:分组和捕获。本文介绍非捕获组 (?:…) 如何只分组不捕获,命名组 (?<year>…) 如何让捕获内容更易读,如何用 \k<name> 引用它们,以及 Python 里的写法。

Regex Hunter 标志

· 阅读约 3 分钟

/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/

抓住

  • 2026-10-13
  • 1999-12-31
  • 2000-01-01
  • 2026-02-28

放过

  • 26-10-13
  • 2026/10/13
  • 2026-1-3
  • 13-10-2026

正则表达式里的括号同时做两件事。它们负责分组,让量词或 | 作用于括号里的全部内容。它们也负责捕获,把匹配到的文本保存下来,供你之后使用。

很多时候你只需要其中一项。本文介绍把这两项工作拆开的两种分组:非捕获组和命名组。每个示例都可以交互,修改正则就能看到各行的变化。

01普通分组会捕获

普通分组会保存它匹配到的内容,并按左括号出现的位置从 1 开始编号:

const m = "2026-10-13".match(/(\d{4})-(\d{2})-(\d{2})/);

m[0];  // "2026-10-13", the whole match
m[1];  // "2026"
m[2];  // "10"
m[3];  // "13"

在正则改动之前,这一切都没问题。可一旦在开头附近加一个分组,后面的编号就全部顺延,m[2] 会悄悄变成别的东西。

02非捕获组:(?:…)

需要分组、但不关心文本时,用 (?: 代替 (。下面这个分组存在的唯一目的,是让 + 重复“一个单词加一个空格”:

匹配4/4 符合预期
/^(?:\w+ )+\w+$/

应当匹配2/2

  • two words
  • one two three

不应匹配2/2

  • single
  • double space
在沙盒里试试
(?: … ) 把“一个单词加一个空格”编成一组,让 + 可以重复它。不捕获任何内容。

它的匹配结果和 (\w+ )+\w+ 完全相同,区别只在于你拿回来的东西:没有需要编号的捕获,改动正则时也不会有编号顺延。它还稍微省一点开销,因为引擎不必保存文本。

一个好习惯:凡是之后不读取的分组,都用 (?:…)。这样剩下的编号组或命名组,就都是真正有意义的。

03命名组:(?…)

给分组起个名字,然后按名字而不是按编号读取它:

匹配5/5 符合预期
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/

应当匹配2/2

  • 2026-10-13
  • 1999-12-31

不应匹配3/3

  • 26-10-13
  • 2026/10/13
  • 2026-1-3
在沙盒里试试
同样的日期正则,只是每个部分都有了名字。它匹配的行和编号版本完全相同。
const { year, month, day } =
  "2026-10-13".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/).groups;

year;  // "2026"

命名组同样有编号,所以无论哪种写法,m[1] 都是年份。名字只是让代码在正则改动后依然有效。

在替换中也能用,写作 $<name>:

"2026-10-13".replace(
  /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
  "$<d>/$<m>/$<y>",
);  // "13/10/2026"

04反向引用:\k

反向引用匹配的是某个分组之前匹配过的同一段文本。对于命名组,写法是 \k<name> 而不是 \1:

匹配4/4 符合预期
/^(?<quote>["']).*\k<quote>$/

应当匹配2/2

  • "hello"
  • 'hello'

不应匹配2/2

  • "hello'
  • 'hello"
在沙盒里试试
结尾的引号必须和开头的引号是同一个字符。

这种写法一看就知道是什么意思,\1 永远做不到这一点;而且即使你在前面加了分组,它依然有效。

05Python 的写法不同

Python 最早支持命名组,写法里带一个 P:

import re

m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-10")
m.group("year")      # "2026"
m.groupdict()        # {"year": "2026", "month": "10"}

re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>/\g<y>", "2026-10")  # "10/2026"
re.match(r"(?P<q>[\"']).*(?P=q)$", '"hi"')  # backreference by name

所以同一个概念,在 JavaScript 里写作 (?<name>…) 和 \k<name>,在 Python 里则是 (?P<name>…)、(?P=name) 和 \g<name>。非捕获组 (?:…) 在哪里都一样。

06重复的分组只保留最后一次

还有一个意外:放在量词里的分组只保留最后一次重复。

"abc".match(/(\w)+/)[1];  // "c", not ["a", "b", "c"]

如果你需要每一次重复,请改为对内部的正则使用 g 修饰符加 matchAll(或 re.findall)。

07试一试

引号必须成对。用一个分组,再加一个指回它的引用:

可玩狩猎标准杆 12

抓住用成对引号包起来的文本:"…" 或 '…'。

目标 · 抓住 0/5

  • "hi"目标
  • 'hi'目标
  • "it's"目标
  • 'say "no"'目标
  • ""目标

无辜词 · 误抓 0

  • "hi'无辜
  • 'hi"无辜
  • hi无辜
  • "hi无辜
  • hi"无辜

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

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

08检查清单

  • ( … ) 既分组又捕获,(?: … ) 只分组。
  • 凡是之后不读取的分组,都用 (?: … )。
  • (?<name> … ) 按名字捕获。用 .groups.name 读取,用 $<name> 替换。
  • \k<name> 再次匹配同一段文本。
  • Python:(?P<name> … )、(?P=name) 和 \g<name>。
  • 重复的分组只保留最后一次重复。

09继续练习