跳到主要内容

字符类详解

字符类(Character Class)是正则表达式中最基础也最重要的概念之一。它允许你定义一个字符集合,匹配其中的任意一个字符。理解字符类的工作原理和各种边界情况,能帮助你写出更精确、更高效的正则表达式。

什么是字符类

字符类用方括号 [...] 表示,它定义了一个字符集合,匹配其中的任意一个字符。注意,是"任意一个",不是"所有"。

// 字符类匹配单个字符
/[abc]/.test("a"); // true,匹配 'a'
/[abc]/.test("b"); // true,匹配 'b'
/[abc]/.test("ab"); // true,匹配 'a'(只匹配一个字符)
/[abc]/.test("d"); // false,'d' 不在集合中

关键理解[abc] 不是匹配 "abc" 这个字符串,而是匹配 a、b 或 c 中任意一个字符。如果你想匹配完整的 "abc",直接写 abc 即可。

基本语法

列举字符

最简单的方式是直接列出所有允许的字符:

// 匹配元音字母
/[aeiou]/.test("apple"); // true,匹配 'a'
/[aeiou]/.test("sky"); // true,匹配 'y' 不是元音,但 'a'、'e'、'i'、'o'、'u' 都不在... 等等
// 实际上 /[aeiou]/.test("sky") 返回 false

// 匹配数字 0-4
/[01234]/.test("3"); // true
/[01234]/.test("5"); // false

字符范围

使用连字符 - 可以表示字符范围,使表达式更简洁:

// 匹配小写字母
/[a-z]/.test("m"); // true
/[a-z]/.test("M"); // false(大写不在范围内)

// 匹配数字
/[0-9]/.test("5"); // true
/[0-9]/.test("a"); // false

// 匹配十六进制字符
/[0-9a-fA-F]/.test("F"); // true
/[0-9a-fA-F]/.test("x"); // false

// 多个范围可以组合
/[a-zA-Z]/.test("G"); // true(任意字母)
/[a-zA-Z0-9]/.test("5"); // true(字母或数字)

范围的本质:字符范围基于字符的 Unicode 码点。[a-z] 匹配码点从 97('a')到 122('z')的所有字符。

// 范围是按码点顺序的
/[A-Z]/.test("M"); // true
/[A-Z]/.test("a"); // false('a' 的码点大于 'Z')

// ASCII 字符的码点顺序
// A-Z: 65-90
// a-z: 97-122
// 0-9: 48-57

否定字符类

在字符类的开头使用 ^ 表示"不在集合中的字符":

// 匹配非数字
/[^0-9]/.test("a"); // true
/[^0-9]/.test("5"); // false

// 匹配非元音字母
/[^aeiou]/.test("b"); // true
/[^aeiou]/.test("a"); // false

// 匹配非空白字符
/[^ \t\n\r]/.test("a"); // true
/[^ \t\n\r]/.test(" "); // false

注意^ 只有在字符类的开头才表示否定,在其他位置就是字面的 ^ 字符:

// ^ 在开头:否定
/[^abc]/.test("a"); // false

// ^ 不在开头:字面字符
/[a^b]/.test("^"); // true
/[a^b]/.test("a"); // true

字符类内部的特殊规则

字符类内部有自己的"特殊字符"规则,与外部不同。理解这些规则能避免很多常见错误。

规则一:大部分元字符失去特殊含义

在字符类外部,. * + ? | $ 等是元字符,有特殊含义。但在字符类内部,它们变成普通字符:

// 在字符类内部,. 就是字面的点号
/[a.c]/.test("a.c"); // true,匹配 'a' 或 '.' 或 'c'
/[a.c]/.test("abc"); // true,匹配 'a'
/[a.c]/.test("aXc"); // false,'X' 不在集合中

// 在字符类内部,* 就是字面的星号
/[ab*]/.test("*"); // true
/[ab*]/.test("a"); // true

// 在字符类内部,+ 就是字面的加号
/[a+b]/.test("+"); // true

// 在字符类内部,? 就是字面的问号
/[a?b]/.test("?"); // true

// 在字符类内部,| 就是字面的竖线
/[a|b]/.test("|"); // true(注意:这不是"或")
/[a|b]/.test("a"); // true
/[a|b]/.test("b"); // true

规则二:必须转义的字符

在字符类内部,只有四个字符需要转义:

字符含义说明
]结束字符类必须转义才能匹配字面的 ]
\转义字符必须转义才能匹配字面的 \
^否定(在开头)不在开头时不需要转义
-范围在开头或结尾时不需要转义
// 匹配方括号
/[\[\]]/.test("["); // true
/[\[\]]/.test("]"); // true

// 匹配反斜杠
/[\\]/.test("\\"); // true

// 匹配脱字符
/[\^]/.test("^"); // true
/[a^]/.test("^"); // true(不在开头,不需要转义)

// 匹配连字符
/[-a]/.test("-"); // true(在开头)
/[a-]/.test("-"); // true(在结尾)
/[a\-z]/.test("-"); // true(转义)
/[a-z]/.test("-"); // false(表示范围,不包含 '-')

规则三:连字符的位置

连字符 - 在字符类中有特殊含义(表示范围),它的位置决定了它是范围操作符还是字面字符:

// 在中间:范围
/[a-z]/.test("m"); // true
/[0-9]/.test("5"); // true

// 在开头或结尾:字面字符
/[-abc]/.test("-"); // true
/[abc-]/.test("-"); // true

// 转义:字面字符
/[a\-z]/.test("-"); // true

常见的连字符错误

// 错误:想匹配 a-z 或 -,但写错了位置
/[a-z-]/.test("-"); // true(- 在结尾,正确)
/[a-z-]/.test("m"); // true

// 危险:范围定义错误
/[a-z-0-9]/.test("-"); // true,但范围是从 'z' 到 '0'?
// 实际上 [a-z-0-9] 包含 a-z、-、0-9,因为 - 在中间形成了 'z' 到 '0' 的范围
// 但 'z'(122) 到 '0'(48) 是反向范围,不同引擎处理不同

// 安全写法
/[a-z\-0-9]/.test("-"); // 使用转义
/[a-z0-9-]/.test("-"); // 放在结尾
/[-a-z0-9]/.test("-"); // 放在开头

规则四:右方括号的处理

右方括号 ] 表示字符类的结束,如果要匹配字面的 ],必须转义:

// 错误:想匹配 ] 但没转义
/[]]/.test("]"); // SyntaxError(空字符类)

// 正确:转义
/[\]]/.test("]"); // true

// 特殊情况:] 在开头可以不转义
/[]a]/.test("]"); // true(空字符类后跟 a?不,这是特殊情况)
// 某些引擎允许 ] 在开头表示字面字符,但不推荐依赖这种行为

预定义字符类

正则表达式提供了预定义的字符类,用于匹配常见的字符类型:

基本预定义类

预定义类含义等价形式
.任意字符(默认不含换行)-
\d数字[0-9]
\D非数字[^0-9]
\w单词字符[a-zA-Z0-9_]
\W非单词字符[^a-zA-Z0-9_]
\s空白字符[ \t\n\r\f\v]
\S非空白字符[^ \t\n\r\f\v]

数字类 \d\D

// \d 匹配数字
/\d/.test("5"); // true
/\d/.test("a"); // false
/\d\d\d/.test("123"); // true

// \D 匹配非数字
/\D/.test("a"); // true
/\D/.test("5"); // false

Unicode 注意事项:在 JavaScript 默认模式下,\d 等价于 [0-9],只匹配 ASCII 数字。但某些语言(如 Python)在 Unicode 模式下,\d 会匹配所有 Unicode 数字字符:

import re

# Python 默认 Unicode 模式
re.search(r'\d', '123') # 匹配全角数字(这取决于实现)
# 全角数字 '1' 是 U+FF11

# 使用 ASCII 标志限制为 ASCII 数字
re.search(r'\d', '123', re.ASCII) # None

单词字符类 \w\W

// \w 匹配字母、数字、下划线
/\w/.test("a"); // true
/\w/.test("Z"); // true
/\w/.test("5"); // true
/\w/.test("_"); // true
/\w/.test("-"); // false
/\w/.test("中"); // false(中文字符不在 \w 范围内)

// \W 匹配非单词字符
/\W/.test("-"); // true
/\W/.test(" "); // true
/\W/.test("a"); // false

匹配中文\w 不匹配中文字符,如果需要匹配中文,使用 Unicode 属性或显式范围:

// 使用 Unicode 属性(ES2018+)
/\p\{Script=Han\}/u.test("中"); // true
/\p\{L\}/u.test("中"); // true(任意 Unicode 字母)

// 使用显式范围
/[\u4e00-\u9fa5]/.test("中"); // true(基本汉字范围)

// 组合使用:匹配中文或英文单词
/[\u4e00-\u9fa5\w]+/.test("hello世界"); // true

空白字符类 \s\S

// \s 匹配空白字符
/\s/.test(" "); // true,空格
/\s/.test("\t"); // true,制表符
/\s/.test("\n"); // true,换行符
/\s/.test("\r"); // true,回车符
/\s/.test("\f"); // true,换页符
/\s/.test("\v"); // true,垂直制表符

// \S 匹配非空白字符
/\S/.test("a"); // true
/\S/.test(" "); // false

// 实际应用:去除首尾空白
const str = " hello world ";
str.replace(/^\s+|\s+$/g, ""); // "hello world"

// 实际应用:分割空白分隔的单词
"hello world\t\ntest".split(/\s+/);
// ["hello", "world", "test"]

点号 . 的特殊性

点号 . 是一个特殊的预定义类,它匹配"任意字符":

// . 匹配任意字符(默认不含换行)
/a.c/.test("abc"); // true
/a.c/.test("aXc"); // true
/a.c/.test("a c"); // true
/a.c/.test("a\nc"); // false(不匹配换行)

// 使用 s 标志让 . 匹配换行
/a.c/s.test("a\nc"); // true

// 或者使用 [\s\S] 匹配任意字符(包括换行)
/a[\s\S]c/.test("a\nc"); // true

为什么 . 默认不匹配换行:这是历史原因。在早期的文本处理工具中,通常按行处理文本,. 的设计目的是匹配"行内"的任意字符。

空字符类和特殊字符类

空字符类

// 空字符类 [] 不匹配任何字符
/[]/.test("a"); // false
/[]/.test(""); // false

// 否定空字符类 [^] 匹配任意字符
/[^]/.test("a"); // true
/[^]/.test("\n"); // true
// [^] 等价于 [\s\S] 或 . 配合 s 标志

字符类中的量词

虽然字符类本身只匹配一个字符,但可以配合量词使用:

// 匹配一个或多个数字
/\d+/.test("12345"); // true

// 匹配 3 个字母
/[a-z]{3}/.test("abc"); // true

// 匹配 0 个或多个单词字符
/\w*/.test("hello_123"); // true

常见陷阱与解决方案

陷阱一:混淆字符类和分组

// 错误:字符类不是分组
/[abc]+/.test("abc"); // true,但匹配的是 'a'、'b'、'c' 中的任意组合
/[abc]+/.test("aaa"); // true(三个 'a')
/[abc]+/.test("cba"); // true(任意顺序)

// 如果要匹配完整的 "abc",使用分组
/(abc)+/.test("abc"); // true
/(abc)+/.test("abcabc"); // true
/(abc)+/.test("aaa"); // false

陷阱二:范围定义错误

// 错误:想匹配大写字母和小写字母,但写成了 [A-z]
/[A-z]/.test("["); // true!([ 的码点是 91,在 Z 和 a 之间)
// ASCII 顺序:A-Z (65-90),然后 [ \ ] ^ _ ` (91-96),然后 a-z (97-122)

// 正确写法
/[A-Za-z]/.test("a"); // true
/[A-Za-z]/.test("["); // false

陷阱三:忘记转义特殊字符

// 错误:想匹配点号但没转义
/[.]/.test("."); // true(在字符类内 . 是字面字符)
/./.test("."); // true(但 . 匹配任意字符,不是我们想要的)
/./.test("a"); // true(. 匹配了 'a')

// 正确:在字符类外转义
/\./.test("."); // true
/\./.test("a"); // false

// 字符类内的点号不需要转义
/[.]/.test("."); // true
/[.]/.test("a"); // false

陷阱四:否定字符类的误用

// 误解:以为 [^abc] 匹配 "不是 abc 的字符串"
/[^abc]/.test("def"); // true(但匹配的是 'd',不是整个 "def")
/[^abc]/.test("a"); // false
/[^abc]/.test("abc"); // true!(匹配 "abc" 中的第一个字符?不...)
// 等等,/[^abc]/.test("abc") 实际上返回什么?
// 返回 true!因为 test() 只要找到任何匹配就返回 true
// 字符类匹配一个字符,"abc" 中没有 a、b、c 以外的字符吗?
// 实际上这里 /[^abc]/ 会在位置 0 尝试匹配 'a',失败
// 位置 1 尝试匹配 'b',失败
// 位置 2 尝试匹配 'c',失败
// 所以 /[^abc]/.test("abc") 返回 false

// 如果要匹配不包含 a、b、c 的字符串
/^[^abc]+$/.test("def"); // true
/^[^abc]+$/.test("abc"); // false

陷阱五:字符类中的嵌套

// 字符类不能嵌套
/[a[bc]d]/.test("a"); // true
/[a[bc]d]/.test("["); // true(字面的 '[')
/[a[bc]d]/.test("b"); // true
// 这个模式实际上是匹配 'a'、'['、'b'、'c'、']'、'd'

// 如果需要"或"的逻辑,使用分组
/(a|bc|d)/.test("bc"); // true

高级用法

在字符类中使用预定义类

// 组合使用
/[\d\s]/.test("5"); // true(数字)
/[\d\s]/.test(" "); // true(空白)
/[\d\s]/.test("a"); // false

// 否定组合
/[^\d\s]/.test("a"); // true
/[^\d\s]/.test("5"); // false

Unicode 属性(ES2018+)

// 匹配任意 Unicode 字母
/\p\{L\}/u.test("中"); // true
/\p\{L\}/u.test("A"); // true

// 匹配汉字
/\p\{Script=Han\}/u.test("中"); // true
/\p\{Script=Han\}/u.test("A"); // false

// 匹配 Emoji
/\p\{Emoji\}/u.test("😀"); // true

v 标志的集合运算(ES2024+)

// 差集:希腊字母排除 Σ
/[\p\{Greek\}--Σ]/v.test("α"); // true
/[\p\{Greek\}--Σ]/v.test("Σ"); // false

// 交集:既是希腊字母又是大写字母
/[\p\{Greek\}&&\p\{Lu\}]/v.test("Ω"); // true
/[\p\{Greek\}&&\p\{Lu\}]/v.test("ω"); // false

// 字符串字面量
/[\q\{ab|cd\}]/v.test("ab"); // true
/[\q\{ab|cd\}]/v.test("a"); // false

性能考虑

具体的字符类比 . 更快

// 较慢:. 匹配任意字符,可能导致不必要的回溯
/<div>.*<\/div>/

// 较快:使用否定字符类,更精确
/<div>[^<]*<\/div>/

避免过大的字符范围

// 不推荐:范围太大
/[\u0000-\uFFFF]/

// 推荐:只匹配需要的范围
/[\u4e00-\u9fa5]/ // 基本汉字

小结

字符类是正则表达式的基础构建块:

  • 基本语法[abc] 匹配 a、b 或 c 中的任意一个
  • 字符范围[a-z] 匹配从 a 到 z 的所有字符
  • 否定字符类[^abc] 匹配不在集合中的字符
  • 预定义类\d\w\s 等是常用的快捷方式
  • 内部规则:大部分元字符在字符类内失去特殊含义
  • 必须转义]\^(在开头)、-(在中间)需要转义

掌握字符类是编写高效正则表达式的关键一步。记住,精确的字符类比宽泛的 . 更安全、更高效