分组与捕获

圆括号在正则里有两个作用:一是分组(把多个字符当一个整体施加量词),二是捕获(把括号匹配到的内容单独存起来,后续可以取出)。这是正则从"匹配"走向"提取"的关键能力,几乎所有数据抽取都靠它。

编号捕获组

圆括号默认既是分组也是捕获组。引擎按左括号出现的顺序给每个组编号,从 1 开始。第 0 个(整体匹配)始终是整个模式匹配到的内容。在替换里可以用美元符号加数字引用。

// 圆括号 (...) 的两个作用:分组 + 捕获

// 1. 分组:把多个字符当一个整体施加量词
/ab+/.test("abbb");      // true  (+ 作用于 b)
/(ab)+/.test("abab");    // true  (+ 作用于整体 ab)

// 2. 捕获:把括号匹配到的内容"存起来",后续可单独取出
// 提取日期里的年月日
const re = /(\d{4})-(\d{2})-(\d{2})/;
const m = "2026-08-05".match(re);
// m[0]  整个匹配  "2026-08-05"
// m[1]  第 1 组   "2026"  (年)
// m[2]  第 2 组   "08"    (月)
// m[3]  第 3 组   "05"    (日)
console.log(m[1], m[2], m[3]);   // 2026 08 05

// 在替换里用 $1 $2 $3 引用捕获组
"2026-08-05".replace(re, "$3/$2/$1");   // "05/08/2026"

提取日期、URL、SQL 字段时这是核心套路:把要提取的字段分别用圆括号括起来,匹配成功后用 m[1] m[2] 逐个取。在替换里用 $1 $2 引用,可以重排字段顺序、转换格式。

命名捕获组

编号捕获在小正则里没问题,但一旦组多了,$3 $5 这种"魔术数字"会让人困惑。命名捕获组给每个组起名字,可读性提升一个量级,生产代码优先用。

// 命名捕获组:给组起名字,可读性大幅提升
// 语法 (?<name>...)  JS 2018+/PCRE/Python/.NET 都支持

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const r = "2026-08-05".match(re);
console.log(r.groups.year);    // "2026"
console.log(r.groups.month);   // "08"
console.log(r.groups.day);     // "05"

// 替换时用 $<name> 引用命名组
"2026-08-05".replace(re, "$<day>/$<month>/$<year>");   // "05/08/2026"

// 在 replace 的回调里直接解构 groups
"2026-08-05".replace(re, (match, ...args) => {
  const { year, month, day } = args.pop();
  return `${day}.${month}.${year}`;
});   // "05.08.2026"

// Python 写法 (?P<name>...)
// import re
// m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-08")
// m.group("year")   // "2026"

注意不同语言的命名语法略不同:JS/.NET 用 (?<name>...) 形式,Python 用 (?P<name>...) 形式(带 P)。引用方式也对应不同,但功能一致。

非捕获组

有时你只需要圆括号的"分组"功能(比如把选择分支括起来),并不需要"捕获"结果。这时用非捕获组(?:...),它只分组不占编号,既保持组编号干净又略微提升性能。

// 非捕获组 (?:...):只分组,不捕获

// 普通捕获组会"占编号",非捕获组不占
/(?:https?|ftp):\/\//.test("https://");   // true
//   ^^^^^^^^^^^^^ 这个括号只用于"应用 | 选择",不存储结果

// 对比:为什么有时需要非捕获
// 用捕获版:编号被无关括号占走,后面想用的组编号变大
/(ab)+\s+(\d+)/.exec("abab 123");
// m[1] = "ab", m[2] = "123"  (如果只想用 123,却被 ab 占了编号 1)

// 用非捕获版:编号只给真正要提取的组
/(?:ab)+\s+(\d+)/.exec("abab 123");
// m[1] = "123"  (只捕获数字部分)

// 性能:非捕获组略快于捕获组(不分配存储)
// 大正则里推荐:不需要提取的分组都加 ?:

一个简单原则:不需要提取的分组都加问号冒号。这样既不会污染组编号,大正则也跑得稍快。读别人的正则时看到 (?:...) 也能立即知道"这里只是为了分组,不需要提取数据"。

反向引用

反向引用(backreference)让你引用前面捕获组匹配到的内容——用来匹配"重复单词"、"成对引号"、"对称标签"这种"前后必须一样"的场景,威力巨大。

// 反向引用(backreference):引用前面捕获的内容
// 语法 \1 \2 ... 或在命名组里用 \k<name>

// 匹配重复单词 "the the" "is is"
/\b(\w+)\s+\1\b/.test("the the end");   // true
/\b(\w+)\s+\1\b/.test("the cat");       // false (前后不一样)

// 匹配成对的引号(单引号或双引号)
/(["']).+?\1/.exec('say "hello"');
// ['"hello"']  (\1 引用开头的同一个引号,确保成对)
// 不会错误匹配 'mixed"  这种不成对的

// 匹配 HTML 标签(开标签和闭标签同名)
/<(\w+)>[\s\S]*?<\/\1>/.exec("<div>content</div>");
// ["<div>content</div>", "div"]

// 命名反向引用(更清晰)
/\b(?<word>\w+)\s+\k<word>\b/.test("go go");
// true

反向引用的经典场景:检测重复单词(写作助手的功能)、匹配成对的引号或括号、解析 HTML 标签(确保开闭标签同名)。命名反向引用(用反斜杠 k 加尖括号包名字的形式)更易读,大正则优先用。

小结

下一章讲选择分支——用竖线实现"或"逻辑,以及它和分组的优先级配合。

← 上一篇 锚点与单词边界

下一篇 选择分支

✈️💬