正则表达式:模式匹配入门 · 第 5 / 6 节

第 5 课:分组与选择

本课目标:

  • 用捕获组 (…) 包住模式的一部分,并读出它捕获的内容。
  • 在匹配数组里找到被捕获的片段(索引 0 是整个匹配,捕获组排在后面)。
  • | 提供备选,并用分组控制 | 的作用范围。

先修条件:第 1-4 课(字符类、量词、锚点) | 上一篇 << 04 | 下一篇 06 >>

日期匹配上了,现在把年份取出来

你现在能写 /^\d{4}-\d{2}-\d{2}$/ 来校验一个日期。但校验不等于提取:知道一个字符串日期,并不会把年份递到你手上。还有,一个模式想同时接受 “gray” 和 “grey”,或者 “cat” 和 “dog”,该怎么办?这两个需求指向同一对元字符。圆括号身兼两职:它把模式的一部分捆在一起,并且记住那部分匹配到的内容;而 | 提供选择。要小心的是,| 的作用范围比多数人预期的更远,而圆括号正是把它管住的办法。

讲解

捕获组(capturing group)是用圆括号包起来的一段模式。它同时做两件事。第一,它把 token 捆成一束,让量词可以作用于整个组:在 /(ab)+/ 里,+ 重复的是 “ab”,匹配 “ab”、“abab”、“ababab”。第二,它记住自己匹配到的内容,供你事后取出:/(foo)/ 匹配并记住 “foo”1

记住的文本去了哪里?进了 match 返回的数组。不带全局标志时,match 把第一个匹配作为数组返回:索引 0 是整个匹配,每个捕获组的文本按顺序排在后面:索引 1 是第一个组,索引 2 是第二个,依此类推2。捕获组的匹配结果与左圆括号的出现顺序一致1

这就是提取的做法:把你想要的部分用圆括号包起来,然后在索引 1、2 等位置读出它们。

有时你想要分组而不想要捕获,比如只是为了让量词作用于一束你并不打算提取的 token。非捕获组(non-capturing group)(?:…) 干的就是这个:它把 token 捆在一起,但不会往结果数组里添加条目1。当圆括号只是为了托住一个量词或一段备选时,就用它。

接下来是选择运算符。析取(disjunction)| 分隔多个备选项;引擎先试左边的备选,失败了再试右边的3。所以 /cat|dog/ 匹配 “cat” 或 “dog”。微妙之处在于优先级,而它会造成真实的 bug:| 在正则里的优先级最低3。这意味着它在尽可能宽的位置把模式劈开。/^cat|dog$/ 不是“整个字符串是 cat 或 dog”;它解析成 (^cat)(dog$):以 “cat” 开头,以 “dog” 结尾。

要让 | 只作用于模式的一小段,就把那一段包进组里3/^(cat|dog)$/ 把备选放进圆括号,锚点便包住了这个选择:整个字符串必须恰好是 “cat” 或恰好是 “dog”。gr(a|e)y 是同一个思路,它匹配 “gray” 或 “grey”,同时把共享的 “gr” 和 “y” 留在选择之外。

完整示例(跟着做)

你想把时间字符串里的小时和分钟取出来,而不只是校验它。把每个片段包进组里,然后读回来:

一步步看:模式是 (\d{2}):(\d{2})。第一个 (\d{2}) 把两位数字捕获进组 1,字面量 : 匹配冒号(它在任何圆括号之外,所以被捕获),第二个 (\d{2}) 把两位数字捕获进组 2。match 之后,索引 0 是完整的 “09:30”,索引 1 是 “09”,索引 2 是 “30”。圆括号把一个校验器变成了一个提取器。

你来试试(填空示例)

你想要一个模式,匹配恰好是 “cat” 或 “dog” 的整个字符串:前后都不能有别的东西。填上分组,让锚点包住这个选择:

答案:空格处填 (cat|dog),得到 /^(cat|dog)$/。圆括号把 | 限制在 “cat” 与 “dog” 之间,锚点 ^$ 于是作用于整个组,整个字符串必须恰好是这两个词之一。没有圆括号的 /^cat|dog$/ 会因为 | 优先级太低而放 “category” 进门,因为它以 “cat” 开头。

小结 + 下节课预告

圆括号构成捕获组:它既把 token 捆起来供量词使用,记住匹配到的文本,这些文本出现在匹配数组的索引 1 及之后(索引 0 永远是整个匹配)。想分组但不想捕获时用 (?:…)| 运算符提供选择,但优先级最低,会在最宽的位置劈开模式:把它包进组里才能控制它的范围。还有一件事没弄清楚:当 + 这样的量词既能匹配短的也能匹配长的时候,它到底拿走多少?下一课我们把贪婪与懒惰讲清楚,然后你会从头到尾亲手搭建并验证一个真实的校验器。

Footnotes

  1. MDN: Groups and backreferences — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Groups_and_backreferences 2 3

  2. MDN: String.prototype.match() — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match

  3. MDN: Disjunction (the | operator) — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Disjunction 2 3

练习

01

在你的控制台里,运行 "file: report.pdf".match(/(\w+)\.(\w+)/)。读出结果的索引 0、1、2 三个元素,并说出每个是什么。

第 1 级(热身)
完成标准 · 本地勾选
02

写一个模式,匹配恰好是 “yes” 或 “no”(区分大小写)的整个字符串,然后提取出给的是哪一个。用 "yes"、"no"、"maybe" 测试。再解释为什么 /^yes|no$/(没有圆括号)是错的。

第 2 级(进阶)
完成标准 · 本地勾选

我的笔记

记下想法、痛点、没懂的地方。只写进这门课的附录,正课文件不动。