打底:它到底在做什么
字符与字符类
它在解决什么
上一篇讲的是引擎怎么动, 这一篇讲最小的那块积木:怎么描述「一个字符」。
后面所有东西(量词、分组、分支)都是在这块积木上叠的——
a+ 是「这个字符重复若干次」,[a-z]+ 是「这一类字符重复若干次」。
积木本身描述错了,叠多高都不对。
普通字符与元字符
正则里的字符分两类:
- 普通字符:
a、中、7——「匹配它自己」。 - 元字符:
. ^ $ * + ? ( ) [ ] { } | \——有特殊含义。
要匹配元字符本身,前面加反斜杠:\. 匹配一个真正的点。
.:不是「任意字符」
. 常被念成「任意字符」,但它有一个例外:
准确的说法是「除换行外的任意字符」。想要真正的任意字符,两个办法:
[\s\S] 空白 ∪ 非空白 = 一切。到处都能用,不依赖修饰符
. 配 s dotAll 修饰符,见「修饰符」那篇
⚠️ 这个例外很容易被测试数据掩盖:手写的测试串通常是单行的, 而真实数据里有换行。「本地能跑、上线匹配不到」有相当一部分是这个。
[]:一个字符的集合
方括号表示「一个字符,且它属于这个集合」:
[abc] a 或 b 或 c
[a-z] a 到 z(按码点连续区间)
[a-zA-Z0-9] 多个区间可以并排写
[^abc] 取反:不是 a、b、c 的那些
🚨 连字符的位置决定它是范围还是字面量:
[a-c] 匹配 a、b、c 三个字母,而 [ac-] 匹配的是 a、c 和连字符本身。
要表达字面的 -,把它放在方括号的开头或结尾(或者写成 \-)。
这条规则不难记,但写错了不报错——你会得到一个能跑、只是匹配范围不对的模式。
🚨 方括号里的规则完全不同
这是本篇最该记住的一条:方括号内外是两套语法。
[.+] 和 .+ 毫不相干——前者是「一个字符,
要么是点要么是加号」,后者是「任意字符,一个或多个」。
在方括号里,绝大多数元字符变回普通字符:. * + ? ( ) { } |
全都是字面量,不用转义。方括号里真正需要留意的只有四个:
| 字符 | 什么时候特殊 | 怎么写字面量 |
|---|---|---|
] |
总是(它结束字符类) | \] 或放在最前面 |
\ |
总是 | \\ |
^ |
只在开头(表示取反) | 放到非开头位置 |
- |
只在两个字符中间 | 放开头或结尾 |
👉 这条规则的实用价值:在方括号里尽量别转义。
[.+*?] 是对的,写成 [\.\+\*\?] 虽然也能跑,但会让人以为那些反斜杠是必需的,
下次遇到 [] 里的 - 反而不知道该怎么办。
🚨 否定类比你想的宽
[^a] 读作「不是 a 的一个字符」。问题在于「不是 a 的字符」包括换行:
/^[^a]$/ 匹配一个换行符——也匹配中文、
制表符、emoji 的半个码元。
⚠️ 注意它和 . 的不对称:
. 排除换行
[^a] 不排除换行
这两个都是「大致上的任意字符」,而它们对换行的态度相反。
用否定类表达「除了 x 的字符」时,先想一遍:换行算不算?
不算就自己写进去:[^a\n]。
📌 这条和实战那篇推荐的
[^\]]+(取方括号里的内容)连起来看:那个写法很好,但如果内容可能跨行、
而你不希望它跨行,就得写成 [^\]\n]+。
\d \w \s 与它们的大写否定
三个常用简写,各自有一个大写的否定版:
| 简写 | 含义 | 否定 |
|---|---|---|
\d |
数字 [0-9] |
\D |
\w |
单词字符 [A-Za-z0-9_] |
\W |
\s |
空白 | \S |
两个必须知道的边界:
\w 不含中文。 它就是 [A-Za-z0-9_] 这 63 个字符,下划线算,连字符不算。
这件事的后果在锚点那篇会放大——\b 的定义依赖 \w,
所以 \b 对中文完全无效。
\s 比「空格和 Tab」宽得多。
/^\s$/ 匹配 U+00A0(不换行空格),而 [ \t] 不匹配。
它还匹配全角空格 U+3000、各种 Unicode 空白。
🚨 这条是真实数据里的常客:从网页复制来的文本常混着 U+00A0,
它在屏幕上和普通空格长得一模一样。用 [ \t] 去 trim 会漏掉它,
用 \s 则能清掉——这也是 \s+ 比 [ ]+ 更该是默认选择的原因。
(JS 的 \d 永远是 [0-9],但 Python 的不是——见方言那篇。)
哪些字符要转义
一条够用的规则:
方括号外面:
. ^ $ * + ? ( ) [ ] { } | \这 14 个要转义,其余不用。 方括号里面:只有] \ ^ -需要留意,其余不用。
如果模式是用字符串拼出来的(new RegExp(s)),还要多一层:
JS 字符串字面量自己会吃掉一层反斜杠,所以 \d 得写成 '\\d'。
这是「同一条正则在字面量里对、拼字符串就不对」的常见原因。
📌 需要把一段用户输入当字面量塞进正则时,别手动转义——
写个小函数把所有元字符前面加反斜杠,或者干脆别用正则
(String.prototype.includes 通常就够了)。
怎么选
| 你要表达的 | 写法 |
|---|---|
| 某几个具体字符之一 | [abc] |
| 一个区间 | [a-z],多个区间并排写 |
| 除了某些字符 | [^abc] —— 先想一遍换行算不算 |
| 任意字符(不含换行) | . |
| 真·任意字符 | [\s\S] |
| 数字 | [0-9] 比 \d 更稳(跨语言时尤其) |
| 空白 | \s,别写 [ \t] |
| 字面的连字符 | 放在方括号开头或结尾 |
下一步
《量词与贪婪/懒惰》—— 有了「一个字符」这块积木,下一步就是说明它重复几次。 那一篇会讲到本教程里最常见的那个坑:量词默认尽可能多拿。
本篇示例
下面每一条都由 npm run test:regex 在每次构建前实跑验证, 结果是现算的,不是抄进数据里的副本。正文里的代码块只用来演示匹配过程和写法对照,不进闸门; 凡是「这个模式配这个输入得到这个结果」的断言,只存在于这里。
.不是「任意字符」,它不包括换行- 调用
/^.$/.test("\n")- 结果
false- 换成
/^[\s\S]$/ true
[\s\S]是「真·任意字符」的惯用写法。另一个办法是加s修饰符,见[修饰符那篇](/regex/practice/flags/)。方括号里,元字符变回普通字符
- 调用
/^[.+]$/.test("ab")- 结果
false- 换成
/^.+$/ true
[.+]和.+是两件毫不相干的事。方括号里.+*?()全是字面量。🚨
[^a]的「否定」比你想的宽得多- 调用
/^[^a]$/.test("\n")- 结果
true- 换成
/^[^a\n]$/ false
⚠️ 注意它和
.的不对称:.排除换行,[^a]不排除。用否定类当「除了 x 的字符」时,先想一遍换行。连字符的位置决定它是范围还是字面量
- 调用
[..."abcd-".matchAll(/[a-c]/g)]- 结果
["a","b","c"]- 换成
/[ac-]/g ["a","c","-"]
要表达字面的连字符,把它放在方括号的开头或结尾,或者转义成
\-。\s比「空格和 Tab」宽得多- 调用
/^\s$/.test(" ")- 结果
true- 换成
/^[ \t]$/ false
🚨 从网页复制来的文本里常混着 U+00A0(不换行空格)和全角空格 U+3000,两者
\s都算、肉眼都看不出来。
练习
先自己写,再看答案。读懂和写得出是两件事,而这一节练的是后者。每道题的参考答案都由 npm run test:exercises-regex 实跑验证: 答案必须通过全部用例,「常见错解」必须至少被一条用例抓住, 而且 /.*/ 这类万能写法必须过不了 —— 否则这道题就没有区分度。
校验「整串不含任何数字」,且不能为空
用 re.test(输入) 判断
输入 期望 "abc"应匹配 "a-b"应匹配 "ab1"不应匹配 ""不应匹配 提示
「不是数字的字符」有一个现成的简写;然后想想要重复几次。
参考答案
/^\D+$/\D就是[^0-9],两种写法都对。⚠️ 真正的坑是那个+—— 没有它,^[^0-9]$只接受长度为 1 的串。常见错解
/^[^0-9]$/—— 它在"abc"这条上就错了。校验十六进制串:只含 0-9、a-f、A-F,至少一位
用 re.test(输入) 判断
输入 期望 "a1F"应匹配 "abc"应匹配 "g1"不应匹配 ""不应匹配 参考答案
/^[0-9a-fA-F]+$/字符类里可以并排写多个区间。⚠️ 想用
i省掉A-F也行,但那会让整条模式都变成大小写不敏感 —— 见[工程化那篇](/regex/practice/engineering/)里i的坑。常见错解
/^[0-9a-f]+$/—— 它在"a1F"这条上就错了。校验「只含字母和连字符」,至少一位
用 re.test(输入) 判断
输入 期望 "a-b"应匹配 "abc"应匹配 "a_b"不应匹配 "a1"不应匹配 提示
-在方括号里有特殊含义,想表达它本身该放哪个位置?参考答案
/^[a-zA-Z-]+$/连字符要表达它自己,得放在字符类的开头或结尾(放中间会被当成范围符号)。写成
[a-zA-Z\-]转义也行。常见错解
/^[a-zA-Z]+$/—— 它在"a-b"这条上就错了。