跳到主要内容

正则表达式 — 字符串里的微型流程图

这一章讲三件事: 正则的词汇表(字符集、重复、分组)怎么拼出模式; 引擎怎么跑模式(流程图 + 回溯),以及贪婪为什么是 Bug 之母; 配套的 API(test/exec/replace/matchAll)各自适合什么场合。 读完你能读懂大多数正则,并知道哪种任务不该用正则。

1. 先看现象:又臭又硬,又极其有用

章首那句话先钉在这儿:「有人遇到问题,说『我知道了,用正则!』——现在他们有两个问题了。」书里对正则的定调是双面的:语法晦涩、JS 给的接口粗糙,但它是处理字符串的强力工具;正确地理解它会让你成为更有效的程序员1

书里还给它一个来历定位:工具以混乱的方式进化存活,胜出的未必是最好的,而是恰好够用、又搭上了成功技术便车的——正则正是搭了各语言便车的小型独立语言2

2. 词汇表:从字符到模式

写法两种:/abc/ 字面量,或 new RegExp("abc")(模式来自字符串时用后者,注意双反斜杠)。最简单的用法是 test,问一句「含不含匹配」:/abc/.test("abcde") → true3

逐级加码:

语法意思例子
[0123456789] / [0-9]集合内任一字符 / 区间/\d/ 是它的捷径
\d \w \s数字 / 「词字符」/ 空白;大写取反. = 换行外的任意字符4
+ / * / ?重复 ≥1 / ≥0 / 可选neighbou?r 两种拼法都中5
{2,4} {5,}精确次数 / 开放区间日期 /\d{1,2}-\d{1,2}-\d{4}/ 比全手写 \d\d 可读多了6
(…)分组,重复运算符可作用于整组/boo+(hoo+)+/i 吼哭声7
a|b多选一/\d+ (pig|cow|chicken)s?/ 数家畜8

3. exec、分组,和一个 Date 实战

test 只答「有没有」;exec 返回匹配对象:像数组(第 0 项是整体匹配),带 index 属性,各分组的匹配依次排在后面9

/'(.+?)'/.exec("she said 'hello'")
// → ["'hello'", "hello"] 整体 + 第 1 组
/bad(ly)?/.exec("bad") // → ["bad", undefined] 组没匹配上 → undefined
/(\d)+/.exec("123") // → ["123", "3"] 组重复多次 → 只留最后一次
/(?:na)+/ // (?: ) 只分组不捕获

配套的 Date 类有两个要背的怪癖:月份从 0 起、日期从 1 起——书里原文「令人困惑且愚蠢,当心」;时间戳(用一个数表示的时刻)是 1970 起的毫秒数(Unix time 的遗产)10。分组一出场就能干活了:从字符串抠出日期并转成 Date 对象——

function getDate(string) {
let [_, month, day, year] = /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
return new Date(year, month - 1, day);
}
getDate("1-30-2003"); // → Thu Jan 30 2003 …

(_ 是惯例:丢掉整体匹配那个占位的元素)11

两个补丁必须记:^$ 锚定字符串首尾——没有它们,上面的 getDate 会从 "100-1-30000" 里也抠出一个「日期」;\b 词边界依赖「词字符」的古老定义,所以不可靠12。还有前瞻 (?=…):要求后面的文本匹配、却不把它吃进结果、也不前进位置;(?!…) 是反义版13

4. 主走查:引擎怎么跑 —— 流程图与回溯

引擎把正则当流程图:能从图左走到图右,即匹配;当前位置从 0 开始,每过一个盒子就核对面前的文本,不行就从下一个起点重来14

岔路怎么走?回溯(backtracking):进岔路时记住来路,此路不通就退回来换一条15。书里的原图模式:/^([01]+b|[\da-f]+h|\d+)$/——二进制数带 b、十六进制带 h、或普通十进制。拿 "103" 走:

① 进「二进制」岔路:1、0 都是 [01],吞到 3 —— 出局!
(b 没来)退回岔路口
② 进「十六进制」岔路:1、0、3 都是 [\da-f],吞完 —— 又出局!(h 没来)
③ 进「十进制」岔路:1、0、3 全收,走到 $ → 匹配成功

重复运算符同样会回溯:/^.*x/"abcxe",.* 先贪整个串,发现没有 x 可配,退一位再试,退到 abc 才等到 x——报告匹配16

回溯也有灾难形态:模式能以多种方式匹配同一段文本时。经典事故是二进制数写成了 /([01]+)+b/——去匹配一长串没有结尾 b 的 0 和 1:内圈吞完→没 b→退一格→外圈再吞→又没 b→再退……每多一个字符,工作量翻一倍;几十个字符就要跑到天荒地老17

5. 贪婪与非贪婪:删注释案例

+ * ? {…} 默认贪婪:尽量多吃,吃崩了再一格格退。书里的 live 火演练习——删掉 JS 代码里的注释:

function stripComments(code) {
return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
stripComments("1 /* a */+/* b */ 1");
// → "1 1" ← 出事了!两个注释之间的代码没了

走查:[^]*(「非空集」,即任意字符)先吃到字符串末尾,然后开始退,一直退到最后一个 */ 才配平——于是第一条注释、中间的代码、第二条注释被一锅端18。处方:加问号变非贪婪(+? *? ?? {…}?),从「尽量少吃」开始,不够再添:

return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
// → "1 + 1" ✓

书里的经验法则原样留下:大量正则 Bug 都能追溯到「本该非贪婪的地方用了贪婪」;用重复运算符时优先考虑非贪婪变体19

6. replace:火力全开的替换

  • 普通替换:"papa".replace("p", "m")"mapa",只换第一处;加 g 旗标全局换20;
  • 分组回引:"Liskov, Barbara\n…".replace(/(\p{L}+), (\p{L}+)/gu, "$2 $1") 把「姓, 名」翻转成「名 姓」——$1 $2 引用分组,$& 是整体;
  • 替换参数可以是函数:每个匹配调一次,分组的匹配当实参。书里的减一演示:"1 lemon, 2 cabbages, and 101 eggs"(\d+) (\p{L}+) 配上 minusOne 函数,得到 "no lemon, 1 cabbage, and 100 eggs"(剩 1 个就去复数、剩 0 个写 no)21;
  • 动态构造模式时,记得先把用户输入转义:name.replace(/[\\[.+*?(){|^$]/g, "\\$&"),否则网名 dea+hl[]rd 直接把正则炸成无意义的形状22

7. lastIndex:全局正则的暗雷

正则对象自己带属性:source 是模式原文,lastIndex 只在带 g/y 旗标、且通过 exec 调用时生效——作者的原话:「更省心的做法本来是给 exec 加个起点参数,但『混乱』本来就是 JS 正则接口的本质特征」23。共享同一个带 g 的正则对象时,残余的 lastIndex 会坑人:

let digit = /\d/g;
digit.exec("here it is: 1"); // → ["1"]
digit.exec("and now: 1"); // → null !! 上次的 lastIndex 残留所致

所以书里的忠告:对全局正则保持警惕,只在 replace 和明确要用 lastIndex 的场合用它;想「找出全部匹配」,用 matchAll(要求 g,返回所有匹配的迭代)24

8. 国际化补课:\w 的负债与 u 旗标

"word character" 的定义停留在史前:26 个拉丁字母 + 数字 + 下划线——é 和 β 都不是「词字符」。\s 反倒因历史意外认全了 Unicode 空白25。书里的裁决:处理可能含非英文文本时用 \w 是负债;要用 \p{L}(任意字母)、\p{Script=Greek} 这类 Unicode(给全球每个字符统一编号的标准)属性类——但必须加 u 旗标才生效(向前兼容的历史枷锁)26。反过来,要拿数字去算的场合就用 \d:Number 转不了「任意文字里的数字字符」。同理,.{3} 默认按码元工作:/🍎{3}/ 不匹配三个苹果,加 u 才把一对码元当一个字符27

9. 收束实战:parseINI

章末把全部语法拧进一个小解析器(还记得第 07 章的 Egg 吗?这是另一种「小语言」):空行与 ; 注释跳过;[节] 开新节;键=值 存进当前节;其余一切行都是非法的,直接抛错28。三个可迁移的细节:

  • 拆行用 /\r?\n/,兼容 Windows 的回车换行;
  • 每条模式的 ^$ 把匹配钉死在整行——书里点名:漏掉锚的正则「大体能用,但对某些输入行为怪异,是难查的 Bug」29;
  • if (match = line.match(…)) 把赋值当条件的惯用法,让 else-if 链保持优雅30

10. 作者的判断与证据

  • 「胜者未必最好,只是搭上车」:技术史观,正则即例证2;
  • 「贪婪是 Bug 之源、优先非贪婪」:经验法则,stripComments 是它给的证据;
  • 「\w 是负债」:立场明确,给了 é/β 反例25;
  • 「利器配笨手柄」:总结句——有些任务正则极大简化;但把它硬塞进它无法干净表达的问题是错的;会用的标志之一是忍住不用31

判断(我们的,不是书里的): 本章最有长期价值的是那个「流程图」心智模型—— 读任何正则,先画岔路,再想回溯路径;灾难性回溯可以直接从图上看出 (两个圈能互相喂料,如 ([01]+)+b)。能用字符串方法(split/indexOf)解决的, 先用字符串方法;正则留给「模式」真正复杂的场合。 如果错,会错在: 复杂的嵌套结构(HTML、JSON)本就超出正则的表达力, 「少用正则」的边界在「模式 vs 结构」那条线上,不在复杂度上。

11. 边界与局限

  • 命名分组 (?<name>…)、Unicode 属性的完整清单书里没展开;
  • 正则性能引擎相关;「灾难回溯」的判别只有示例,没有系统方法;
  • INI 例子刻意忽略引号值、多行值等真实 INI 变体;
  • 书中接口以 2024 版 JS 为准(如 matchAll),更老环境不适用。

12. 可带走的

  1. 正则=模式的微型语言;字面量 /…/new RegExp(动态内容先转义);
  2. 词汇表:集合 []、捷径 \d \w \s、重复 + * ? {n,m}、分组 ()、多选 |;
  3. exec 给整体+分组+index;没匹配的组是 undefined;重复组只留最后一次;(?: ) 不捕获;
  4. Date:月份 0 起、日期 1 起;时间戳=1970 起毫秒;
  5. ^ $ 钉死首尾;\b 不可靠;(?= ) 零宽;
  6. 引擎=流程图+回溯;([01]+)+b 式双圈是性能炸弹;
  7. 贪婪尽量多吃;默认选非贪婪 +? *?;
  8. replace 三段火力:g 全局、$1 回引、函数当替换;
  9. 带 g 的正则对象有状态(lastIndex),别共享复用;找全部用 matchAll;
  10. 处理人名地名先想 \p{L} + u;\w 只认拉丁字母;
  11. 每条面向「行」的正则,先补上 ^$

13. 原文地图

主题原书章原文位置
两个问题(题词)与进化史观Regular Expressionstext/12-fm-regular-expressions.txt:5(搜「two problems」) · :9(搜「chaotic, evolutionary」)
又笨拙又有用同上text/12-fm-regular-expressions.txt:13(搜「terribly awkward」)
字面量与构造器同上text/12-fm-regular-expressions.txt:19(搜「RegExp」) · :26(搜「backslash」)
test 与集合同上text/12-fm-regular-expressions.txt:34(搜「abcde」) · :51(搜「0-9」)
捷径表同上text/12-fm-regular-expressions.txt:56(搜「shortcuts」)
重复与可选同上text/12-fm-regular-expressions.txt:127(搜「plus sign」) · :140(搜「optional」) · :148(搜「{4}」)
吼哭声分组同上text/12-fm-regular-expressions.txt:162(搜「cartoonCrying」)
家畜多选一同上text/12-fm-regular-expressions.txt:275(搜「animalCount」)
exec 与分组三例同上text/12-fm-regular-expressions.txt:174(搜「exec」) · :195(搜「bad(ly)」) · :200(搜「?:」)
Date 的月份陷阱同上text/12-fm-regular-expressions.txt:223(搜「confusing and silly」)
getDate 实战同上text/12-fm-regular-expressions.txt:240(搜「getDate」)
锚与词边界同上text/12-fm-regular-expressions.txt:254(搜「caret」) · :256(搜「word boundaries」)
前瞻同上text/12-fm-regular-expressions.txt:260(搜「Look-ahead」) · :262(搜「braeburn」)
流程图机制同上text/12-fm-regular-expressions.txt:287(搜「flow diagram」)
回溯与 103同上text/12-fm-regular-expressions.txt:295(搜「103」) · :297(搜「backtracks」)
.*x 与灾难回溯同上text/12-fm-regular-expressions.txt:301(搜「abcxe」) · :305(搜「doubles」)
replace 与 $1同上text/12-fm-regular-expressions.txt:316(搜「Borobudur」) · :325(搜「Liskov」)
函数替换减一同上text/12-fm-regular-expressions.txt:336(搜「lemon」)
贪婪删注释事故同上text/12-fm-regular-expressions.txt:364(搜「stripComments」) · :371(搜「four characters」)
非贪婪处方与忠告同上text/12-fm-regular-expressions.txt:378(搜「] *?」) · :383(搜「nongreedy」)
动态构造与转义同上text/12-fm-regular-expressions.txt:389(搜「harry」) · :401(搜「escaped」)
lastIndex 与共享陷阱同上text/12-fm-regular-expressions.txt:425(搜「essential feature」) · :448(搜「digit」)
g 改变 match、matchAll同上text/12-fm-regular-expressions.txt:202(搜「Banana」) · :463(搜「matchAll」)
\w 负债与 \p同上text/12-fm-regular-expressions.txt:94(搜「rather dumb」) · :110(搜「liability」)
u 旗标与 emoji同上text/12-fm-regular-expressions.txt:544(搜「🍎」) · :553(搜「u (Unicode)」)
parseINI 与整行锚定同上text/12-fm-regular-expressions.txt:507(搜「parseINI」) · :534(搜「whole line」)
赋值当条件同上text/12-fm-regular-expressions.txt:536(搜「assignment」)
利器笨手柄同上text/12-fm-regular-expressions.txt:606(搜「sharp tool」)

Footnotes

  1. 出处:「Regular Expressions」第 5 段(text/12-fm-regular-expressions.txt:5,搜「two problems」)与第 13 段(text/12-fm-regular-expressions.txt:13,搜「terribly awkward」)。

  2. 出处:「Regular Expressions」第 9 段(text/12-fm-regular-expressions.txt:9,搜「chaotic, evolutionary」)。 2

  3. 出处:「Regular Expressions」第 19 段(text/12-fm-regular-expressions.txt:19,搜「RegExp」)与第 34 段(text/12-fm-regular-expressions.txt:34,搜「test」)。

  4. 出处:「Regular Expressions」第 56 段(text/12-fm-regular-expressions.txt:56,搜「shortcuts」)。

  5. 出处:「Regular Expressions」第 127 段(text/12-fm-regular-expressions.txt:127,搜「plus sign」)与第 142 段(text/12-fm-regular-expressions.txt:142,搜「neighbou」)。

  6. 出处:「Regular Expressions」第 148 段(text/12-fm-regular-expressions.txt:148,搜「{2,4}」)与第 150 段(text/12-fm-regular-expressions.txt:150,搜「easier to decipher」)。

  7. 出处:「Regular Expressions」第 162 段(text/12-fm-regular-expressions.txt:162,搜「cartoonCrying」)。

  8. 出处:「Regular Expressions」第 275 段(text/12-fm-regular-expressions.txt:275,搜「animalCount」)。

  9. 出处:「Regular Expressions」第 172 段(text/12-fm-regular-expressions.txt:172,搜「exec」)与第 187 段(text/12-fm-regular-expressions.txt:187,搜「first element」)。

  10. 出处:「Regular Expressions」第 223 段(text/12-fm-regular-expressions.txt:223,搜「confusing and silly」)与第 227 段(text/12-fm-regular-expressions.txt:227,搜「Unix time」)。

  11. 出处:「Regular Expressions」第 240 段(text/12-fm-regular-expressions.txt:240,搜「getDate」)与第 248 段(text/12-fm-regular-expressions.txt:248,搜「underscore」)。

  12. 出处:「Regular Expressions」第 254 段(text/12-fm-regular-expressions.txt:254,搜「caret」)与第 256 段(text/12-fm-regular-expressions.txt:256,搜「word boundaries」)。

  13. 出处:「Regular Expressions」第 260 段(text/12-fm-regular-expressions.txt:260,搜「Look-ahead」)与第 267 段(text/12-fm-regular-expressions.txt:267,搜「negative look-ahead」)。

  14. 出处:「Regular Expressions」第 285 段(text/12-fm-regular-expressions.txt:285,搜「from the start of the string」)与第 287 段(text/12-fm-regular-expressions.txt:287,搜「flow diagram」)。

  15. 出处:「Regular Expressions」第 297 段(text/12-fm-regular-expressions.txt:297,搜「backtracks」)。

  16. 出处:「Regular Expressions」第 301 段(text/12-fm-regular-expressions.txt:301,搜「abcxe」)。

  17. 出处:「Regular Expressions」第 305 段(text/12-fm-regular-expressions.txt:305,搜「doubles」)。

  18. 出处:「Regular Expressions」第 371 段(text/12-fm-regular-expressions.txt:371,搜「back from there」)。

  19. 出处:「Regular Expressions」第 373 段(text/12-fm-regular-expressions.txt:373,搜「greedy」)与第 383 段(text/12-fm-regular-expressions.txt:383,搜「nongreedy」)。

  20. 出处:「Regular Expressions」第 312 段(text/12-fm-regular-expressions.txt:312,搜「mapa」)与第 314 段(text/12-fm-regular-expressions.txt:314,搜「global」)。

  21. 出处:「Regular Expressions」第 330 段(text/12-fm-regular-expressions.txt:330,搜「$1」)与第 336 段(text/12-fm-regular-expressions.txt:336,搜「lemon」)。

  22. 出处:「Regular Expressions」第 396 段(text/12-fm-regular-expressions.txt:396,搜「dea+hl」)与第 401 段(text/12-fm-regular-expressions.txt:401,搜「escaped」)。

  23. 出处:「Regular Expressions」第 423 段(text/12-fm-regular-expressions.txt:423,搜「lastIndex」)与第 425 段(text/12-fm-regular-expressions.txt:425,搜「essential feature」)。

  24. 出处:「Regular Expressions」第 448 段(text/12-fm-regular-expressions.txt:448,搜「digit」)与第 459 段(text/12-fm-regular-expressions.txt:459,搜「cautious」)、第 47 段(text/12-fm-regular-expressions.txt:47,搜「matchAll」)。

  25. 出处:「Regular Expressions」第 94 段(text/12-fm-regular-expressions.txt:94,搜「rather dumb」)与第 110 段(text/12-fm-regular-expressions.txt:110,搜「liability」)。 2

  26. 出处:「Regular Expressions」第 100 段(text/12-fm-regular-expressions.txt:100,搜「\p{L}」)与第 98 段(text/12-fm-regular-expressions.txt:98,搜「u character」)。

  27. 出处:「Regular Expressions」第 121 段(text/12-fm-regular-expressions.txt:121,搜「Number」)与第 544 段(text/12-fm-regular-expressions.txt:544,搜「🍎」)。

  28. 出处:「Regular Expressions」第 495 段(text/12-fm-regular-expressions.txt:495,搜「Blank lines」)与第 538 段(text/12-fm-regular-expressions.txt:538,搜「throws an exception」)。

  29. 出处:「Regular Expressions」第 534 段(text/12-fm-regular-expressions.txt:534,搜「whole line」)。

  30. 出处:「Regular Expressions」第 536 段(text/12-fm-regular-expressions.txt:536,搜「assignment」)。

  31. 出处:「Regular Expressions」第 606 段(text/12-fm-regular-expressions.txt:606,搜「sharp tool」)。