字符与转义
详情 →| 语法 | 含义 · 示例 |
|---|---|
| abc | 按顺序排列的字符 a、b、c |
| . | 除换行符以外的任意字符 |
| \. \* \\ | 按字面匹配的元字符 |
| \t \n \r | 制表符、换行符、回车符 |
字符类与简写
详情 →| 语法 | 含义 · 示例 |
|---|---|
| [abc] | a、b、c 中的一个 |
| [^abc] | 除 a、b、c 以外的任意字符 |
| [a-z] | 范围:任意小写字母 |
| \d \D | 数字 / 非数字 |
| \w \W | 单词字符 [A-Za-z0-9_] / 非单词字符 |
| \s \S | 空白字符 / 非空白字符 |
| \p{L} | 任何文字中的任意字母(需加 u 修饰符) |
锚点
详情 →| 语法 | 含义 · 示例 |
|---|---|
| ^ | 文本开头(用 m 时为行首) |
| $ | 文本结尾(用 m 时为行尾) |
| \b | 单词边界 |
| \B | 非单词边界 |
量词
详情 →| 语法 | 含义 · 示例 |
|---|---|
| * | 零次或多次 |
| + | 一次或多次 |
| ? | 可选:零次或一次 |
| {3} | 恰好 3 次 |
| {2,5} | 2 到 5 次 |
| {2,} | 2 次或更多 |
| *? +? ?? | 懒惰:尽可能少 |
分组与反向引用
详情 →| 语法 | 含义 · 示例 |
|---|---|
| (abc) | 捕获组 |
| (?:abc) | 非捕获组 |
| (?<name>abc) | 命名捕获组 |
| \1 | 第 1 组捕获的文本 |
| \k<name> | 命名分组捕获的文本 |
| a|b | a 或 b |
环视
详情 →| 语法 | 含义 · 示例 |
|---|---|
| (?=abc) | 后面紧跟 abc(先行断言) |
| (?!abc) | 后面不跟 abc |
| (?<=abc) | 前面紧挨 abc(后行断言) |
| (?<!abc) | 前面不是 abc |
修饰符
详情 →| 语法 | 含义 · 示例 |
|---|---|
| i | 忽略大小写 |
| g | 全局:所有匹配,而不只是第一个 |
| m | 多行:^ 和 $ 作用于每一行 |
| s | dotall:. 也匹配换行符 |
| u | Unicode:\p{…} 和 \u{…} |
| y | 粘性:只在 lastIndex 处匹配 |
规则与示例
01 正则表达式如何工作
正则表达式(regex)是一种用来描述文本的小型语言。引擎带着你的正则和输入文本从左到右一起前进,尝试让正则的每一部分都对上字符串中的字符。如果能对上,这个字符串就匹配了。
几乎所有操作都围绕两个问题:
逐个字符匹配
像 cat 这样的正则表示“一个 c,接着一个 a,再接着一个 t,紧挨在一起”。
重复几次
像 + 和 * 这样的量词,规定前一个元素可以重复多少次。
默认情况下,只要在字符串的任意位置能匹配上,正则就算匹配——除非你用锚点把它固定住,否则它不必覆盖整个字符串。
在游戏中(JavaScript)
02 字面字符
大多数字符都匹配它们自己。字母、数字和空格都是字面字符:regex 匹配文本“regex”,42 匹配“42”。匹配默认区分大小写,所以 Cat 不匹配“cat”(i 修饰符可以改变这一点)。
练习学院: 狩猎入门
03 转义元字符
元字符是正则中具有特殊含义的十几个字符:
. ^ $ * + ? ( ) [ ] { } | \\ /
想按字面匹配其中一个,就在它前面加一个反斜杠。\. 匹配真正的点号,\+ 匹配真正的加号,\\ 匹配真正的反斜杠。在字符类里,这些字符大多失去特殊作用,不需要转义。
04 点号 ( . )
点号 . 匹配除换行符以外的任意单个字符(除非开启了 s / dotall 修饰符)。它是正则里最常被滥用的符号——当你真正想表达“任意字母”或“任意数字”时,用字符类更稳妥。
这里 c.t 要求 c 和 t 之间恰好有一个字符,所以“ct”(零个)和“caat”(两个)都不匹配。
练习学院: 狩猎入门
05 字符类 [ ]
方括号定义一个集合:恰好匹配其中的一个字符。[aeiou] 匹配任意一个元音字母。用连字符表示范围:[a-z]、[0-9]、[A-Fa-f0-9](一个十六进制数字)。以 ^ 开头表示取反:[^0-9] 就是“任何不是数字的字符”。
| 语法 | 含义 |
|---|---|
| [abc] | a、b 或 c 中的一个 |
| [a-z] | 任意小写字母 |
| [0-9a-fA-F] | 一个十六进制数字 |
| [^aeiou] | 除小写元音以外的任意字符 |
| [.\-] | 字面的点号或连字符(在字符类里,大多数元字符都按字面意义匹配) |
易错点
[-a]、[a-])。要包含 ],就转义它([\]a]):在 JavaScript 中 [] 是空字符类,所以 []a] 不起作用。^ 只有作为第一个字符时才表示取反。06 简写字符类
简写字符类是常用字符集的单字母简写,比如 \d 表示任意数字。大写字母表示对应小写字母的取反。
| 语法 | 含义 |
|---|---|
| \d \D | 一个数字 [0-9] / 非数字 |
| \w \W | 一个单词字符 [A-Za-z0-9_] / 非单词字符 |
| \s \S | 空白字符(空格、制表符、换行符……)/ 非空白字符 |
| \t \n \r | 制表符、换行符、回车符 |
在游戏中(JavaScript)
\d 和 \w 只匹配 ASCII,即使加上 u 修饰符也一样;其他文字请用 \p{L} 或 \p{N}。\s 本来就能匹配 Unicode 空白。在正则高尔夫里,它们比完整写出一个字符类短得多。练习学院: 字符类
07 锚点与边界
锚点匹配的是位置,而不是字符——它们的宽度为零。
| 语法 | 含义 |
|---|---|
| ^ | 字符串的开头(使用 m 修饰符时为行首) |
| $ | 字符串的结尾(使用 m 修饰符时为行尾) |
| \b | 单词边界——\w 与非 \w 之间的交界 |
| \B | 不是单词边界的位置 |
用 ^…$ 包住正则,就能强制它匹配整个字符串。\bcat\b 匹配完整的单词“cat”,但不匹配“category”或“scatter”。
08 量词
量词规定前一个元素可以重复多少次。
| 语法 | 含义 |
|---|---|
| * | 零次或多次 |
| + | 一次或多次 |
| ? | 零次或一次(可选) |
| {3} | 恰好 3 次 |
| {2,5} | 2 到 5 次 |
| {2,} | 2 次或更多 |
量词作用于紧挨在它前面的东西——一个字符、一个字符类或一个分组。colou?r 让 u 变成可选(匹配“color”和“colour”);(ab)+ 重复这一对字符。
09 贪婪、懒惰与占有
默认情况下,量词是贪婪的:先尽可能多地匹配,如果正则的其余部分需要,再把字符退回去。加上 ? 就能让它变成懒惰的——尽可能少地匹配。
经典例子:对于 <a><b>,贪婪的 <.*> 会匹配整串内容,而懒惰的 <.*?> 在第一个 > 处就停下。
占有量词与原子组(JavaScript 不支持)
练习学院: 量词
10 分支与分组
| 表示“或”,优先级最低——除非用括号把它包起来,否则它会把整个正则一分为二。cat|dog 匹配两个单词中的任意一个;gr(a|e)y 匹配“gray”或“grey”,而 gra|ey 的意思则是“gra”或“ey”。
11 分组:捕获与命名
括号有两个作用:把多个元素分组,让量词或 | 作用于整组;同时捕获匹配到的内容,供之后再用。
| 语法 | 含义 |
|---|---|
| (…) | 捕获组——从左到右编号为 1、2、3…… |
| (?:…) | 非捕获组——只分组,不记录 |
| (?<name>…) | 命名捕获组 |
只需要分组时就用 (?:…)(更快,也能让分组编号保持整齐);当一个正则有好几个值得标注的捕获时,就给它们起名字。
12 反向引用
反向引用匹配的是某个分组已经捕获的同一段文本。\1 指第 1 组,\2 指第 2 组,\k<name> 指一个命名分组。要求内容重复,就靠它:
| 语法 | 含义 |
|---|---|
| (.)\1 | 任意字符后面紧跟它自己——双写字母 |
| (.)(.)\2\1 | 4 个字符的回文(例如“abba”) |
| (.).*\1 | 同一个字符在后面再次出现 |
反向引用 vs. 字符类
(.)\1 要求同一个字符出现两次——这和 [a-z][a-z](任意两个字母)不同。正是反向引用让正则能够检查“同样的东西又出现了一次”,这是普通字符类表达不了的。练习学院: 分组与反向引用
13 先行断言与后行断言
环视用来断言当前位置旁边是否出现某些内容,但不消耗任何字符——它是一个零宽度的条件。
| 语法 | 含义 |
|---|---|
| (?=…) | 正向先行断言——后面紧跟 … |
| (?!…) | 负向先行断言——后面不跟 … |
| (?<=…) | 正向后行断言——前面紧挨 … |
| (?<!…) | 负向后行断言——前面不是 … |
像“包含一个数字和一个字母”这样的密码规则,会在开头叠加多个先行断言:^(?=.*\d)(?=.*[a-z]).+$。每个 (?=…) 都从位置 0 开始检查整个字符串,然后匹配继续进行。在大多数引擎中,后行断言要求固定(或有上限的)宽度。
在游戏中(JavaScript)
14 修饰符
修饰符是改变整个正则匹配方式的单个字母,比如 i 表示不区分大小写。在代码里,它们写在结尾斜杠之后(/cat/gi)。
| 语法 | 含义 |
|---|---|
| i | 忽略大小写 |
| g | 全局——找出所有匹配,而不只是第一个 |
| m | 多行——^ 和 $ 在每个换行处都能匹配 |
| s | dotall——. 也匹配换行符 |
| u | Unicode 模式(启用 \u{…} 和 \p{…}) |
| y | 粘性——只在 lastIndex 处匹配 |
| x | 扩展/详细模式:忽略空白并允许注释(PCRE/Python 支持;JS 不支持) |
15 Unicode 与属性
加上 u 修饰符,就可以按 Unicode 属性匹配:\p{L} 是任何文字中的任意字母,\p{N} 是任意数字,\p{Emoji} 是表情符号,\P{…} 表示取反。\u{1F600} 按十六进制值匹配一个码点。
不加 u 时,正则按 UTF-16 码元工作,所以一个表情符号或其他辅助平面字符会被算作两个“字符”——这通常不是你想要的。属性转义才是处理真实文本的干净办法。
引擎支持
u)、PCRE、Python(通过 regex 模块)、Java 等都支持属性转义,但具体的属性名称各不相同。为了可移植性,请使用常见的那些(L、N、P、文字名称)。16 常用正则
这些是适用于日常格式的简短、可复用正则:整数、小数、邮箱、十六进制颜色和 ISO 日期。把“校验”类正则当作近似方案——真正的邮箱和 URL 规则远比一行正则复杂。
| 语法 | 含义 |
|---|---|
| ^\d+$ | 整个字符串是一个非负整数 |
| -?\d+(\.\d+)? | 可带负号的小数 |
| ^[\w.+-]+@[\w-]+\.[\w.-]+$ | 大致的邮箱格式 |
| ^#?[0-9a-fA-F]{6}$ | 6 位十六进制颜色值 |
| ^\d{4}-\d{2}-\d{2}$ | ISO 日期(YYYY-MM-DD) |
17 写出更短正则的技巧
正则高尔夫的艺术,就是写出最短的正则,抓住每个目标、放过每个要放过的词。棋盘只问“它在任意位置匹配吗”——所以你很少需要锚点,也很少需要描述整个单词。
- 找出最小的区别特征。如果每个目标都包含
q,而要放过的词都不包含,答案就是q。 - 优先用字符类代替分支:
[cb]at胜过cat|bat。 - 简写很短:用
\d代替[0-9],用\w代替[A-Za-z0-9_]。 - 除非某个要放过的词逼你加,否则别用锚点。只有在需要排除一个几乎相同的词时,才加上
^或$。 - 用
?让差异变成可选,而不是开分支。 - 当目标共有一种要放过的词所没有的重复时,就用反向引用。
试着把它改短——cat 和 bat 都是目标,“rat”要放过:
准备好一较高下了吗?每日狩猎、练习包和沙盒都是实战的好地方。
练习学院: 高尔夫技巧
18 各引擎的差异
正则方言是不同正则引擎各自的语法变体,它们支持的功能并不完全相同。主要的几大家族是 JavaScript、PCRE(PHP,与 Perl 接近)、Python(re / regex)和 Java。
| 语法 | 含义 |
|---|---|
| 后行断言 | JS(ES2018+)、PCRE、Python、Java——大多要求固定宽度;PCRE/Python 更宽松 |
| 占有 / 原子 | PCRE、Java、Python regex——JavaScript 不支持 |
递归 (?R) | 仅 PCRE 和 Python regex——JS 和 Python re 都不支持 |
| 命名分组 | JS/PCRE/.NET 用 (?<n>);传统 Python 用 (?P<n>) |
| x / 详细模式修饰符 | PCRE、Python、Java——JavaScript 不支持 |
在游戏中(JavaScript)
19 性能与回溯
大多数正则都很快,但对同一批字符嵌套使用量词可能会爆炸。(a+)+$ 面对一长串“a”再加一个“!”时,会迫使引擎尝试指数级数量的拆分方式——这就是灾难性回溯。
- 避免在量词里再套一个能匹配同样内容的量词(
(a+)+、(.*)*)。 - 让内部各部分互斥,这样就只有一种匹配方式。
- 用
^/$锚定,并优先使用具体的字符类而不是.,以缩小搜索空间。 - 在支持它们的引擎中,占有量词 / 原子组可以彻底阻止回溯。