分组与回溯引用
括号给模式带来哪两件事?
- 语法:
(...)把一段模式包成"捕获组"; - 效果一: 匹配到的内容额外出现在结果数组里, 可单独取出;
- 效果二: 量词作用于整个括号, 而不是只作用于最后一个字符;
// go+ : g 后跟一个或多个 o
"Gogogo now!".match(/(go)+/gi); // Gogogo
- 对比:
go+能匹配goooo;(go)+匹配的是gogo、gogogo;
如何用分组拼出域名与邮箱?
- 域名: 若干"词 + 点", 最后接一个词 →
(\w+\.)+\w+; - 缺陷:
\w不含连字符,my-site.com漏匹配 → 换成[\w-], 得([\w-]+\.)+\w+; - 邮箱:
name@domain, name 允许-与.→[-.\w]+@([\w-]+\.)+[\w-]+; - 现实提醒: 正则只能挡住笔误, 邮箱真伪只能靠真发一封信验证;
"site.com my.site.com".match(/(\w+\.)+\w+/g); // site.com, my.site.com
"my@mail.com @ his@site.com.uk".match(/[-.\w]+@([\w-]+\.)+[\w-]+/g);
结果数组里的分组是怎么编号的?
- 顺序: 按左括号出现的先后, 从左到右;
result[0]: 整个匹配, 永远占 0 号位;result[1]、result[2]、…: 第 1、2、… 个左括号捕获的内容;
const tag = "<h1>Hello, world!</h1>".match(/<(.*?)>/);
tag[0];
tag[1]; // <h1>, h1 — 整个匹配, 第一个组的内容
嵌套分组的编号规律是什么?
- 规律: 仍按左括号从左到右, 与嵌套层级无关;
const r = '<span class="my">'.match(/<(([a-z]+)\s*([^>]*))>/);
r[0];
r[1]; // '<span class="my">', 'span class="my"' — 整个匹配, 第 1 个左括号
r[2];
r[3]; // 'span', 'class="my"' — 第 2、3 个左括号
- 读法: 每个左括号算一次"开单", 开单顺序就是编号顺序, 忽略右括号位置;
可选组没匹配到时结果里是什么?
- 规则: 组仍占位, 值为
undefined; 数组长度固定, 与实际匹配无关;
"a".match(/a(z)?(c)?/); // ['a', undefined, undefined], 长度 3
"ac".match(/a(z)?(c)?/); // ['ac', undefined, 'c'], 长度 3
- 坑: 不能用
length判断"匹配到几个组", 必须逐个比对undefined; - 附带信息: 匹配数组还带
index(匹配位置) 与input(源串);regexp.exec在无g时返回同样结构;
命名组如何免去数括号?
- 写法: 左括号后紧跟
?<name>, 即(?<name>...); - 取值: 命名组同时放进匹配结果的
.groups对象;
const re = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const { year, month, day } = "2019-04-30".match(re).groups; // 2019 04 30
- 取全部匹配: 加
g后str.match会丢弃分组, 需改用matchAll, 见 正则方法与回溯风险; 组数多时名字比序号可读得多;
分组内容如何用在替换字符串里?
- 按号引用:
$n, n 为组号; - 按名引用:
$<name>;
"John Bull".replace(/(\w+) (\w+)/, "$2, $1"); // Bull, John
"2019-10-30".replace(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/, "$<day>.$<month>.$<year>"); // 30.10.2019
- 记号区分: 模式内部引用用反斜杠
\1, 替换串里用美元$1; - 替换串的其他占位符与函数式替换: 见 正则方法与回溯风险;
非捕获组 (?:...) 有什么用?
- 写法: 左括号后跟
?:; - 作用: 只用来施加量词, 不单独占结果位置, 也不参与编号;
- 副作用: 无法被
\N引用, 也无法在替换串里引用;
const result = "Gogogo John!".match(/(?:go)+ (\w+)/i);
result[0]; // Gogogo John
result[1]; // John
result.length; // 2, 没有多出来的 'go'
怎样在模式内部引用刚捕获的文本?
- 按号:
\N, N 为组号; 按名:\k<name>; - 动机: 配对定界符, 例如开闭引号必须是同一种;
const str = `He said: "She's the one!".`;
str.match(/['"](.*?)['"]/g); // ["She' → 引号混配, 错
str.match(/(['"])(.*?)\1/g); // "She's the one!" → 引号配对, 对
str.match(/(?<quote>['"])(.*?)\k<quote>/g); // 同上, 按名引用
- 机制: 引擎记住
(['"])实际匹配到的那个字符,\1要求后面出现完全相同的文本; - 限制:
(?:...)的内容不被记忆, 因此无法引用;
交替 | 的匹配范围由什么决定?
- 语义:
|就是"或", 优先级最低, 范围一直延伸到所在模式的边界; - 与方括号的区别:
[ae]只能选字符或字符类,A|B|C可选任意子表达式; - 等价与陷阱:
gr(a|e)y等价于gr[ae]y, 都匹配gray或grey;gra|ey则匹配gra或ey, 模式被从中间切开;I love HTML|CSS匹配I love HTML或CSS;I love (HTML|CSS)才两条都带I love;
- 结论: 想限定
|的范围, 必须用括号把它括起来;
用交替写时间校验要注意什么?
- 目标: 只接受
00:00–23:59, 拒绝25:99; - 小时: 首位
0/1时次位任取 →[01]\d; 首位2时次位限0-3→2[0-3]; 其他首位非法; - 分钟:
[0-5]\d; - 陷阱: 直接拼成
[01]\d|2[0-3]:[0-5]\d,|把分钟只分给了第二个分支; - 修正: 用括号把"小时"整体括住 →
([01]\d|2[0-3]):[0-5]\d;
"00:00 10:10 23:59 25:99 1:2".match(/([01]\d|2[0-3]):[0-5]\d/g);
// 00:00, 10:10, 23:59
- 规律: 谁被括进括号, 谁才共享括号后面的模式;