第 6 课:贪婪与懒惰匹配,以及最终实战
本课目标:
- 解释
.+为什么贪婪、为什么尽可能多地匹配。- 用
?把量词变懒惰,让它尽可能少地匹配。- 在真实控制台里写出并验证一个校验器兼提取器。
先修条件:第 1-5 课(全套工具) | 上一篇 << 05
模式匹配上了,却抓得太多
你写了 /".+"/ 想抓一个带引号的词,在 say "hi" and "bye" 上一跑,拿回来的不是 “hi”,而是 "hi" and "bye":从第一个引号到最后一个引号的整段。模式没有坏,它只是在精确执行 + 从一开始就注定要做的事。这是正则最著名的一个意外,也是横在你和读懂真实模式之间的最后一个概念:量词默认是贪婪的。一旦你能看见这种贪婪,并且能把它关掉,你粘贴了多年的 .+? 终于说得通了。然后我们把六课所学合在一起,搭一个真东西。
讲解
默认情况下,*、+ 这样的量词是贪婪(greedy)的:它们尽可能多地重复匹配1。贪婪的 + 让引擎把前面的 token 重复尽可能多次2。所以 /".+"/ 在 say "hi" and "bye" 上不会停在第一个闭引号:.+ 一路吃下去(中间的引号也吃,因为 . 匹配它们),一直吃到它还能吃到的最后一个引号那里,再往回退出刚好够让末尾的 " 匹配的部分。
要反转它,在量词后面紧跟一个 ?。这让它变成懒惰(lazy,非贪婪)的:一旦达到最少匹配次数就停下1。懒惰的 .+? 把点重复尽可能少的次数2。现在引擎每吃一个字符就检查模式剩余部分能否匹配,非继续不可才继续,所以它停在第一个闭引号上。
教科书式的演示是一对相邻的方括号标签。对字符串 [one][two],贪婪的 /\[.+\]/ 一口吞下全部,懒惰的 /\[.+?\]/ 只抓第一个标签。(方括号要用 \ 转义,因为 [ 和 ] 是字符类的元字符。)
这里要仔细读 ?,因为它现在有你见过的两份工作。单独跟在一个 token 后面时,? 是第 3 课的量词“0 次或 1 次”。紧跟在另一个量词后面时(+?、*?、{2,5}?),它是懒惰修饰符。同一个字符,两种角色:它扮演哪一种,取决于它左边站的是什么。
完整示例(跟着做)
你想要字符串里第一个 HTML 标签的文本,而不是一路到最后一个标签的所有内容。先看贪婪怎么失败,再用懒惰修好它:
一步步看:<.+> 从第一个 “<” 起步,贪婪的 .+ 在还能给 “>” 留出余地的前提下尽量向右跑:那是最后一个 “>”,所以索引 0 的元素是整个字符串。加上 ?(<.+?>)之后,.+? 拿得越少越好:吃下第一个 “<” 之后,它消耗一个 “b”,立刻发现闭合的 “>” 已经能满足,于是停下,得到 “<b>”。同样的字符,相反的胃口。
你来试试(填空示例)
你想从 total (net) and (gross) 里抓出第一个括号注记,也就是 “(net)”,而不是 “(net) and (gross)”。填上让量词变懒惰的那一个字符:
答案:空格处填 ?,得到 /\(.+?\)/。\( 和 \) 是转义后的字面量圆括号(要转义,因为圆括号是分组的元字符)。贪婪的 .+ 会跑到最后一个 “)”,抓下 “(net) and (gross)”;这个 ? 让 .+? 停在第一个 “)”,你便得到 “(net)”。这种“转义定界符、中间变懒惰”的形状,正是从长字符串里提取第一段被定界内容的标准做法。
小结 + 下一步
量词默认贪婪:.+ 能匹配多少就匹配多少;量词后面紧跟的 ? 让它变懒惰,匹配得尽可能少。这一个字符,就是“抓住第一段被定界的内容”和“吞掉一整行”的差别。至此你已经集齐了整门模式语言:字面量、字符类、量词、锚点、分组、备选,以及贪婪与懒惰。现在再遇到陌生的正则,你可以一个 token 一个 token 地读,而不是粘贴过来赌运气。下面的 mentor-action 把方向反过来:把你刚搭好的校验器交给 agent,让它就着每个 token 考你。
Footnotes
-
MDN: Quantifiers — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Quantifiers ↩ ↩2
-
regular-expressions.info: Repetition (greedy and lazy) — https://www.regular-expressions.info/repeat.html ↩ ↩2
练习
打开你的 Node REPL 或 DevTools 控制台,用六课里的构件从零搭一个日期工具。不要从网上粘贴:自己写,然后验证。
第 2 级(最终实战:亲手搭建并验证一个真实模式)- 写一个校验器,只在整个字符串是 ISO 日期
YYYY-MM-DD(四位数字、连字符、两位数字、连字符、两位数字,再无其他)时为真。 - 写一个提取器,把年、月、日分别拉进独立的捕获组。
- 用下面三个字符串验证两者,每个先预测结果:
"2024-01-15"(合法)、"2024-1-15"(不合法:月份只有一位)、"see 2024-01-15 today"(校验器必须拒绝它,但提取器应当仍能从中找到日期)。
我的笔记
记下想法、痛点、没懂的地方。只写进这门课的附录,正课文件不动。