分组与捕获
圆括号在正则里有两个作用:一是分组(把多个字符当一个整体施加量词),二是捕获(把括号匹配到的内容单独存起来,后续可以取出)。这是正则从"匹配"走向"提取"的关键能力,几乎所有数据抽取都靠它。
编号捕获组
圆括号默认既是分组也是捕获组。引擎按左括号出现的顺序给每个组编号,从 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 加尖括号包名字的形式)更易读,大正则优先用。
小结
- 圆括号
(...)既是分组也是捕获,按左括号顺序编号。 - 命名组提升可读性,生产代码优先用。
- 非捕获组
(?:...)只分组不占编号,推荐默认使用。 - 反向引用匹配"前后一致"的内容,成对结构必备。
下一章讲选择分支——用竖线实现"或"逻辑,以及它和分组的优先级配合。
← 上一篇 锚点与单词边界
下一篇 选择分支 →