打底:它到底在做什么

字符与字符类

它在解决什么

上一篇讲的是引擎怎么动, 这一篇讲最小的那块积木:怎么描述「一个字符」。

后面所有东西(量词、分组、分支)都是在这块积木上叠的—— 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 在每次构建前实跑验证, 结果是现算的,不是抄进数据里的副本。正文里的代码块只用来演示匹配过程和写法对照,不进闸门; 凡是「这个模式配这个输入得到这个结果」的断言,只存在于这里。

  1. . 不是「任意字符」,它不包括换行

    调用
    /^.$/.test("\n")
    结果
    false
    换成 /^[\s\S]$/
    true

    [\s\S] 是「真·任意字符」的惯用写法。另一个办法是加 s 修饰符,见[修饰符那篇](/regex/practice/flags/)。

  2. 方括号里,元字符变回普通字符

    调用
    /^[.+]$/.test("ab")
    结果
    false
    换成 /^.+$/
    true

    [.+] 和 .+ 是两件毫不相干的事。方括号里 . + * ? ( ) 全是字面量。

  3. 🚨 [^a] 的「否定」比你想的宽得多

    调用
    /^[^a]$/.test("\n")
    结果
    true
    换成 /^[^a\n]$/
    false

    ⚠️ 注意它和 . 的不对称:. 排除换行,[^a] 不排除。用否定类当「除了 x 的字符」时,先想一遍换行。

  4. 连字符的位置决定它是范围还是字面量

    调用
    [..."abcd-".matchAll(/[a-c]/g)]
    结果
    ["a","b","c"]
    换成 /[ac-]/g
    ["a","c","-"]

    要表达字面的连字符,把它放在方括号的开头或结尾,或者转义成 \-。

  5. \s 比「空格和 Tab」宽得多

    调用
    /^\s$/.test(" ")
    结果
    true
    换成 /^[ \t]$/
    false

    🚨 从网页复制来的文本里常混着 U+00A0(不换行空格)和全角空格 U+3000,两者 \s 都算、肉眼都看不出来。

练习

先自己写,再看答案。读懂和写得出是两件事,而这一节练的是后者。每道题的参考答案都由 npm run test:exercises-regex 实跑验证: 答案必须通过全部用例,「常见错解」必须至少被一条用例抓住, 而且 /.*/ 这类万能写法必须过不了 —— 否则这道题就没有区分度。

  1. 校验「整串不含任何数字」,且不能为空

    用 re.test(输入) 判断

    输入期望
    "abc"应匹配
    "a-b"应匹配
    "ab1"不应匹配
    ""不应匹配
    提示

    「不是数字的字符」有一个现成的简写;然后想想要重复几次。

    参考答案

    /^\D+$/

    \D 就是 [^0-9],两种写法都对。⚠️ 真正的坑是那个 + —— 没有它,^[^0-9]$ 只接受长度为 1 的串。

    常见错解 /^[^0-9]$/ —— 它在"abc"这条上就错了。

  2. 校验十六进制串:只含 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"这条上就错了。

  3. 校验「只含字母和连字符」,至少一位

    用 re.test(输入) 判断

    输入期望
    "a-b"应匹配
    "abc"应匹配
    "a_b"不应匹配
    "a1"不应匹配
    提示

    - 在方括号里有特殊含义,想表达它本身该放哪个位置?

    参考答案

    /^[a-zA-Z-]+$/

    连字符要表达它自己,得放在字符类的开头或结尾(放中间会被当成范围符号)。写成 [a-zA-Z\-] 转义也行。

    常见错解 /^[a-zA-Z]+$/ —— 它在"a-b"这条上就错了。