Skip to content

正则表达式系统学习文档 ​

资料核对日期:2026-06-30
适合人群:前端、Node.js、Python、Java、全栈开发者、数据清洗与日志分析学习者
学习目标:从字符匹配、分组、断言、替换到性能与工程实践,形成跨语言可迁移的正则表达式能力。


1. 正则表达式是什么 ​

正则表达式(Regular Expression,常简称 Regex 或 RegExp)是一种用来描述字符串匹配规则的小型语言。它不是某一种编程语言独有的能力,而是广泛存在于 JavaScript、Python、Java、Go、Rust、SQL、Nginx、Linux 命令、编辑器搜索、日志平台中的通用工具。

正则表达式常用于:

  • 校验输入:手机号、邮箱、密码、URL、日期格式。
  • 提取内容:从日志、HTML 片段、文件名、接口响应中提取字段。
  • 搜索定位:在代码编辑器、命令行、日志系统中快速查找模式。
  • 替换清洗:批量重命名、删除多余空白、格式化文本。
  • 分割文本:按多个分隔符切分字符串。

它最适合处理“线性文本模式”,但不适合承担完整语法解析器的职责。遇到 JSON、HTML、SQL、编程语言源码这类有嵌套结构的内容,优先使用专门解析器。


2. 学习路线 ​

建议按下面顺序学习:

  1. 先理解“普通字符”和“元字符”的区别。
  2. 掌握字符类:\d、\w、\s、[...]、[^...]。
  3. 掌握量词:*、+、?、{m,n}。
  4. 掌握锚点与边界:^、$、\b。
  5. 掌握分组、捕获、命名捕获、反向引用。
  6. 掌握贪婪、惰性与回溯。
  7. 掌握常用断言:前瞻、后顾。
  8. 学会在 JavaScript、Python、Java 中调用正则。
  9. 学会用正则做提取、替换、校验、切分。
  10. 理解性能风险,避免灾难性回溯和 ReDoS。

3. 快速入门 ​

3.1 第一个正则 ​

正则表达式:

regex
hello

可以匹配字符串中的 hello。

JavaScript 示例:

javascript
const text = "hello regex";
console.log(/hello/.test(text)); // true

Python 示例:

python
import re

text = "hello regex"
print(bool(re.search(r"hello", text)))  # True

Java 示例:

java
import java.util.regex.Pattern;

public class Demo {
    public static void main(String[] args) {
        System.out.println(Pattern.compile("hello").matcher("hello regex").find());
    }
}

3.2 普通字符与元字符 ​

普通字符按字面含义匹配,例如 a 匹配字符 a。

元字符具有特殊含义:

字符含义
.匹配除换行外的任意字符,具体行为受模式影响
*前一个元素出现 0 次或多次
+前一个元素出现 1 次或多次
?前一个元素出现 0 次或 1 次,也可使量词变惰性
^字符串开头,或多行模式下的行开头
$字符串结尾,或多行模式下的行结尾
[]字符类
()分组
{}次数范围
``
\转义

如果要匹配元字符本身,需要转义:

regex
\.

这个表达式匹配真正的点号 .。


4. 字符与转义 ​

4.1 常见转义 ​

写法含义
\d数字,通常等价于 [0-9],具体 Unicode 行为因语言而异
\D非数字
\w单词字符,通常包含字母、数字、下划线
\W非单词字符
\s空白字符,如空格、制表符、换行
\S非空白字符
\n换行
\t制表符
\\反斜杠本身

4.2 字符串转义与正则转义 ​

很多语言里正则先经过“字符串字面量”解释,再交给正则引擎解释,因此可能需要双重转义。

JavaScript 使用正则字面量时:

javascript
const re = /\d+\.\d+/;

JavaScript 使用字符串构造时:

javascript
const re = new RegExp("\\d+\\.\\d+");

Python 推荐使用原始字符串:

python
re.search(r"\d+\.\d+", "version 1.2")

Java 字符串也需要双重转义:

java
Pattern.compile("\\d+\\.\\d+");

经验:能用正则字面量或原始字符串时,就尽量减少双重转义带来的心智负担。


5. 字符类 ​

字符类用于描述“某一位置可以出现哪些字符”。

5.1 基础字符类 ​

regex
[abc]

匹配 a、b、c 中任意一个。

regex
[0-9]

匹配任意一个数字。

regex
[a-zA-Z_]

匹配英文字母或下划线。

5.2 排除字符类 ​

regex
[^0-9]

匹配任意一个非数字字符。

注意:^ 在字符类开头表示“排除”;在字符类外面通常表示“开头”。

5.3 字符类中的特殊字符 ​

在 [] 里面,部分字符的特殊含义会变化:

字符在字符类中的注意点
-表示范围;想匹配 - 本身时放在开头或结尾,或写成 \-
^放在开头表示排除;其他位置多为普通字符
]需要转义,或放在特定位置
.通常只是普通点号

示例:匹配十六进制字符。

regex
[0-9a-fA-F]

示例:匹配文件名中常见安全字符。

regex
[a-zA-Z0-9._-]+

6. 量词 ​

量词用于描述前一个元素出现的次数。

写法含义
a*a 出现 0 次或多次
a+a 出现 1 次或多次
a?a 出现 0 次或 1 次
a{3}a 出现正好 3 次
a{2,5}a 出现 2 到 5 次
a{2,}a 出现至少 2 次

示例:匹配整数。

regex
[+-]?\d+

含义:

  • [+-]?:可选的正号或负号。
  • \d+:一个或多个数字。

示例:匹配简单小数。

regex
[+-]?\d+(\.\d+)?

含义:

  • 先匹配整数部分。
  • (\.\d+)? 表示可选的小数部分。

7. 贪婪、惰性与占有式量词 ​

7.1 贪婪匹配 ​

默认量词是贪婪的,会尽可能多地匹配。

regex
<.+>

匹配文本:

html
<span>hello</span><span>regex</span>

结果可能从第一个 < 一直匹配到最后一个 >。

7.2 惰性匹配 ​

在量词后加 ?,表示尽可能少地匹配。

regex
<.+?>

它会更倾向于匹配第一个标签片段,例如 <span>。

常见惰性量词:

贪婪惰性
**?
++?
???
{m,n}{m,n}?

7.3 占有式量词 ​

部分正则引擎支持占有式量词,例如 Java 支持 *+、++、?+、{m,n}+。它会吃掉尽可能多的内容并拒绝回溯,可用于优化性能。

JavaScript 传统正则不支持占有式量词;Python 标准库 re 在现代版本中支持原子分组和占有式量词相关能力时,也要注意运行时版本。跨语言学习时不要默认所有引擎都支持。

7.4 原子分组 ​

原子分组(atomic group)(?>...) 匹配后「锁定」结果,一旦内部匹配成功就拒绝回溯回去重新尝试。它和占有式量词解决的是同一个问题:消除无意义的回溯。

Java 语法示例:

java
// 普通写法:内部失败时会回溯
String re = "a(bc|b)c";

// 原子分组写法:(?>bc|b) 匹配成功后不再回溯
String atomic = "a(?>bc|b)c";

对文本 abc:普通写法中 (bc|b) 先试 bc,后面再匹配 c 时失败,于是回溯改试 b,最终成功;原子分组里 (?>bc|b) 一旦选定 bc 就不回头,abc 会整体失败(无法回头改选 b)。

⚠️ 原子分组可能改变匹配结果,只应在「确定不需要回溯」时使用。它主要用来防止灾难性回溯(见第 16 章)。

支持情况:

  • Java:支持 (?>...)。
  • Python 3.11+:支持 (?>...) 和占有式量词(*+、++ 等)。
  • JavaScript:至今不支持原子分组。
  • Rust regex crate、Go regexp:用线性时间引擎,根本不需要原子分组。

8. 锚点与边界 ​

锚点不消耗字符,只描述位置。

写法含义
^字符串开头;多行模式下也可表示行开头
$字符串结尾;多行模式下也可表示行结尾
\b单词边界
\B非单词边界

8.1 整体校验 ​

如果要校验整个字符串,而不是只判断其中是否包含某段内容,需要使用开头和结尾锚点。

regex
^\d{6}$

匹配 6 位数字,例如邮政编码、短信验证码。

JavaScript:

javascript
const re = /^\d{6}$/;
console.log(re.test("123456"));  // true
console.log(re.test("abc123456")); // false

8.2 单词边界 ​

regex
\bcat\b

可以匹配独立单词 cat,但不匹配 concatenate 中的 cat。

注意:\b 对中文没有“中文词语边界”的语义。处理中文分词时,应使用分词库或更明确的规则。


9. 分组、捕获与反向引用 ​

9.1 普通分组 ​

regex
(ab)+

匹配 ab 重复一次或多次,例如 ab、abab。

9.2 捕获分组 ​

捕获分组会把匹配到的内容保存下来,供后续提取或替换使用。

regex
(\d{4})-(\d{2})-(\d{2})

匹配日期 2026-06-30,并捕获年、月、日。

JavaScript:

javascript
const match = "2026-06-30".match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // 2026
console.log(match[2]); // 06
console.log(match[3]); // 30

Python:

python
import re

match = re.search(r"(\d{4})-(\d{2})-(\d{2})", "2026-06-30")
print(match.group(1))  # 2026
print(match.group(2))  # 06
print(match.group(3))  # 30

9.3 非捕获分组 ​

如果只想分组,不想捕获,使用 (?:...)。

regex
^(?:jpg|jpeg|png|gif)$

这样可以避免无意义捕获,提高可读性。

9.4 命名捕获 ​

命名捕获让结果更容易理解。

JavaScript:

javascript
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = "2026-06-30".match(re);
console.log(match.groups.year); // 2026

Python:

python
match = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "2026-06-30")
print(match.group("year"))  # 2026

Java:

java
Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher matcher = pattern.matcher("2026-06-30");
if (matcher.find()) {
    System.out.println(matcher.group("year"));
}

9.5 反向引用 ​

反向引用可以匹配前面捕获过的内容。

regex
(['"]).*?\1

匹配由相同引号包裹的字符串,例如 "hello" 或 'hello'。

含义:

  • (['"]) 捕获单引号或双引号。
  • .*? 匹配中间内容。
  • \1 要求结尾引号与第 1 个捕获组相同。

10. 或、优先级与组合 ​

10.1 或 ​

regex
cat|dog

匹配 cat 或 dog。

如果只想让一部分参与“或”,使用分组:

regex
I like (cat|dog)s?

可以匹配:

  • I like cat
  • I like cats
  • I like dog
  • I like dogs

10.2 优先级 ​

正则表达式大致优先级:

  1. 转义与字符。
  2. 字符类。
  3. 量词。
  4. 连接。
  5. 或 |。

示例:

regex
ab|cd

表示 ab 或 cd,不是 a 后面跟 b 或 c 再跟 d。

想明确范围时使用分组:

regex
a(b|c)d

匹配 abd 或 acd。


11. 断言 ​

断言也是一种“位置判断”,它不消耗字符。

11.1 正向前瞻 ​

regex
\d+(?=元)

匹配后面紧跟 元 的数字,但不包含 元。

文本:

text
价格 199元,库存 20件

结果:199。

11.2 负向前瞻 ​

regex
\d+(?!元)

匹配后面不是 元 的数字。

11.3 正向后顾 ​

regex
(?<=¥)\d+

匹配前面是 ¥ 的数字,但不包含 ¥。

11.4 负向后顾 ​

regex
(?<!¥)\d+

匹配前面不是 ¥ 的数字。

11.5 兼容性注意 ​

前瞻支持广泛;后顾在不同语言和运行时中的支持差异更大。Java、Python、现代 JavaScript 支持常见后顾,但具体限制不同。例如某些引擎要求后顾长度固定,某些旧版浏览器或运行时不支持。

写跨环境代码时,优先选择更简单、更兼容的表达方式。


12. 常用标志 ​

不同语言的标志命名不同,但语义相近。

12.1 JavaScript 常用标志 ​

标志含义
g全局匹配
i忽略大小写
m多行模式,影响 ^ 和 $
sdotAll,让 . 可匹配换行
uUnicode 模式
y粘连匹配,从指定位置继续
d返回匹配索引信息
v更强的 Unicode 集合能力,适合现代 Unicode 场景

示例:

javascript
const text = "A\nb";
console.log(text.match(/^b/m)); // ["b"]

12.2 Python 常用标志 ​

标志含义
re.I / re.IGNORECASE忽略大小写
re.M / re.MULTILINE多行模式
re.S / re.DOTALL让 . 匹配换行
re.X / re.VERBOSE允许写多行带注释的正则

示例:

python
pattern = re.compile(r"""
    ^                 # 开头
    [a-z0-9._%+-]+    # 用户名
    @
    [a-z0-9.-]+       # 域名
    \.[a-z]{2,}
    $                 # 结尾
""", re.I | re.X)

12.3 Java 常用标志 ​

标志含义
Pattern.CASE_INSENSITIVE忽略大小写
Pattern.MULTILINE多行模式
Pattern.DOTALL让 . 匹配换行
Pattern.COMMENTS允许带注释的正则
Pattern.UNICODE_CASEUnicode 大小写相关增强

13. 匹配、提取、替换、切分 ​

13.1 JavaScript ​

匹配判断:

javascript
/^\d{6}$/.test("123456");

提取:

javascript
const text = "订单号:A1024,金额:199元";
const match = text.match(/订单号:([A-Z]\d+)/);
console.log(match?.[1]); // A1024

全量提取:

javascript
const text = "a1 b22 c333";
const numbers = [...text.matchAll(/\d+/g)].map(item => item[0]);
console.log(numbers); // ["1", "22", "333"]

替换:

javascript
const normalized = "hello    regex".replace(/\s+/g, " ");
console.log(normalized); // hello regex

切分:

javascript
const parts = "js, python;java rust".split(/[,\s;]+/);
console.log(parts); // ["js", "python", "java", "rust"]

13.2 Python ​

匹配判断:

python
bool(re.fullmatch(r"\d{6}", "123456"))

提取:

python
match = re.search(r"订单号:([A-Z]\d+)", "订单号:A1024,金额:199元")
if match:
    print(match.group(1))

全量提取:

python
re.findall(r"\d+", "a1 b22 c333")  # ["1", "22", "333"]

替换:

python
re.sub(r"\s+", " ", "hello    regex")

切分:

python
re.split(r"[,\s;]+", "js, python;java rust")

13.3 Java ​

匹配判断:

java
boolean ok = Pattern.matches("\\d{6}", "123456");

提取:

java
Pattern pattern = Pattern.compile("订单号:([A-Z]\\d+)");
Matcher matcher = pattern.matcher("订单号:A1024,金额:199元");
if (matcher.find()) {
    System.out.println(matcher.group(1));
}

替换:

java
String normalized = "hello    regex".replaceAll("\\s+", " ");

切分:

java
String[] parts = "js, python;java rust".split("[,\\s;]+");

14. 常见场景表达式 ​

这些表达式适合学习和二次调整,不建议不加思考地直接复制到所有业务中。

14.1 去除首尾空白 ​

regex
^\s+|\s+$

更推荐使用语言内置方法:

javascript
"  hello  ".trim();

14.2 合并多个空白 ​

regex
\s+

替换为一个空格。

14.3 简单邮箱校验 ​

regex
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$

说明:真实邮箱规范非常复杂。业务系统里通常使用“宽松正则 + 邮件验证链接”更稳妥。

14.4 中国大陆手机号基础校验 ​

regex
^1[3-9]\d{9}$

说明:号段规则会变化,强校验时应由后端配置或运营规则维护。

14.5 URL 粗略校验 ​

regex
^https?:\/\/[^\s/$.?#].[^\s]*$

说明:URL 语法复杂,工程中优先使用 URL 构造器或成熟库。

JavaScript:

javascript
function isUrl(value) {
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

14.6 日期格式校验 ​

regex
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$

说明:这个表达式只能校验基础格式和大致范围,不能判断闰年,也不能判断 2026-02-31 这种不存在的日期。真实日期应交给日期库或语言内置日期对象处理。

14.7 变量名校验 ​

简化版 JavaScript 标识符:

regex
^[A-Za-z_$][A-Za-z0-9_$]*$

说明:完整 JavaScript 标识符支持更多 Unicode 字符,这里只覆盖常见英文变量名。

14.8 提取 Markdown 链接 ​

regex
\[([^\]]+)\]\(([^)]+)\)

可以从 [标题](https://example.com) 中提取标题和 URL。

注意:Markdown 支持嵌套和转义,复杂场景应使用 Markdown 解析器。

14.9 提取日志字段 ​

日志:

text
2026-06-30 10:20:30 INFO userId=1024 action=login cost=35ms

表达式:

regex
^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+userId=(\d+)\s+action=(\w+)\s+cost=(\d+)ms$

15. JavaScript、Python、Java 差异速览 ​

能力JavaScriptPython reJava java.util.regex
正则字面量支持 /.../不支持不支持
原始字符串不支持,常用字面量规避推荐 r"..."不支持
命名捕获写法(?<name>...)(?P<name>...)(?<name>...)
全局匹配g 标志、matchAllfindall、finditerMatcher.find() 循环
替换引用$1、$<name>\1、\g<name>$1、${name}
多行模式mre.MPattern.MULTILINE
dotAllsre.SPattern.DOTALL
Unicodeu、v 等能力默认对 Unicode 友好需要注意相关 flag
后顾断言现代环境支持支持,但有固定长度等限制支持,也有规则限制

跨语言迁移时,不要只复制表达式;还要检查:

  • 字符串转义是否需要加倍。
  • 标志写法是否相同。
  • 命名捕获语法是否相同。
  • 替换语法是否相同。
  • Unicode、换行、边界行为是否符合预期。

16. 性能与安全 ​

16.1 回溯是什么 ​

大多数常用正则引擎采用回溯机制。简单理解:当一个路径匹配失败时,引擎会回到前面的某个选择点,尝试另一种匹配方式。

回溯让正则表达式很灵活,但某些写法会导致匹配时间急剧增长。

16.2 灾难性回溯 ​

危险示例:

regex
^(a+)+$

当输入是很多个 a 后面跟一个不匹配字符时,例如:

text
aaaaaaaaaaaaaaaaaaaa!

引擎可能尝试大量组合,导致 CPU 被长时间占用。

16.3 ReDoS ​

ReDoS(Regular Expression Denial of Service)是利用低效正则造成拒绝服务的攻击。只要正则用于处理用户可控输入,就要考虑这个风险。

高风险特征:

  • 嵌套量词:(a+)+、(.*)*。
  • 过宽的 .* 与复杂分支组合。
  • 多个可选分支存在大量重叠。
  • 对超长用户输入不设限制。

16.4 优化原则 ​

  1. 能具体就不要宽泛:用 [^\n]* 代替不必要的 .*。
  2. 能限制长度就限制长度:例如 {1,64}。
  3. 避免嵌套可变长度量词。
  4. 把最容易失败的条件放前面。
  5. 对用户输入设置最大长度。
  6. 高频正则提前编译复用。
  7. 复杂文本优先使用解析器,而不是正则硬扛。

示例:匹配双引号字符串。

不够稳的写法:

regex
".*"

更明确的写法:

regex
"[^"\n]*"

如果要支持转义引号,可以写成:

regex
"(?:\\.|[^"\\])*"

16.5 正则引擎:回溯 vs 线性时间 ​

正则引擎大致分两类:

text
回溯引擎(backtracking,NFA 风格)
- 代表:JavaScript、Python re、Java java.util.regex、PCRE
- 支持反向引用、后顾等「正则」扩展能力
- 最坏情况可能是指数级时间,存在灾难性回溯风险

线性时间引擎(DFA / RE2 风格)
- 代表:Rust regex crate、Go regexp、RE2
- 保证 O(n) 线性时间,不受输入长度影响
- 但放弃反向引用、后顾等能力(无法表达的语言不支持)

为什么 Rust/Go 的正则没有 ReDoS:它们的引擎用「模拟 DFA / 无回溯」算法,保证在输入长度上线性时间,从设计上消灭了灾难性回溯。代价是这些引擎不支持反向引用和部分后顾,因为这些能力本质上需要回溯。

选择建议:

  • 处理不可信用户输入、且语言支持线性引擎(Rust/Go)时,优先用线性引擎。
  • 在 JavaScript/Python/Java 中,通过「避免嵌套量词、限制长度、用原子分组」来降低回溯风险。

16.6 编译与复用 ​

正则「编译」是指把字符串模式解析成引擎内部可执行的结构。编译有成本,高频调用应复用编译结果。

JavaScript:正则字面量在脚本加载时编译一次,循环中直接复用:

javascript
const re = /\d+/;   // 编译一次
for (const line of lines) {
  if (re.test(line)) { /* ... */ }
}

Python:re 模块内部有缓存(缓存最近编译的模式),但显式 re.compile 更清晰,高频场景可复用:

python
re_date = re.compile(r"^\d{4}-\d{2}-\d{2}$")   # 编译一次
for line in lines:
    if re_date.search(line):
        ...

Java:Pattern 本身就是编译产物,务必复用:

java
private static final Pattern DATE = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
// 循环里只用 DATE.matcher(line),不要每次 Pattern.compile

⚠️ String.matches()、String.split()、replaceAll() 每次调用都会重新编译,循环里使用这些方法要警惕性能。

16.7 Unicode 属性类与大小写折叠 ​

第 4 章的 \d、\w 等在不同语言的 Unicode 行为不同。要精确匹配 Unicode 类别,用「属性类」\p{...}。

Java / Python regex 模块 / PCRE 支持 Unicode 属性:

java
// 匹配任意汉字(Unicode 属性 Script=Han)
String han = "\\p{IsHan}+";

// 匹配任意字母(不限 ASCII)
String letters = "\\p{L}+";

常见属性:

写法含义
\p{L}任意字母(Letter)
\p{N}任意数字(Number)
\p{P}标点
\p{IsHan}汉字(Java)
\p{Script=Han}汉字(Python regex 模块 / PCRE)

⚠️ 大小写折叠(case folding):i 标志下 \w、字符类是否匹配非 ASCII 字母,取决于引擎和标志。例如 Java 需要 Pattern.UNICODE_CASE 配合 CASE_INSENSITIVE 才能对非 ASCII 字母做 Unicode 大小写折叠;JavaScript 的 i 默认按 Unicode 折叠,但 u 标志会影响部分行为。处理国际化文本时必须实测。

JavaScript 用 \p{...} 需要 u 标志:

javascript
const re = /\p{Script=Han}+/u;   // 需要 u 标志

16.8 $ 锚点与 \r\n 的坑 ​

$ 在不同引擎里对「行尾换行符」的处理有细微差异,Windows 的 \r\n 最容易踩坑。

问题:很多引擎(尤其 JavaScript)的 $ 会匹配「\n 之前」的位置,导致对 \r\n 结尾的文本,$ 匹配在 \r 和 \n 之间,让 \r 意外留在匹配结果里。

示例(JavaScript):

javascript
const line = "abc\r\n";
console.log(line.match(/abc$/m));  // ["abc"],$ 匹配在 \r 之前,\r 没进结果

但用「行尾断言 + 反向」做替换时,\r 可能残留。跨平台处理行尾时更稳妥的做法是显式处理 \r:

regex
(?:^|\r?\n)      # 行首:字符串开头,或可选 \r 后跟 \n

或者直接用语言的分行/trim 能力,而不是用 $ 硬猜行尾。

Java 里 $ 默认匹配「输入结尾」,启用 MULTILINE 后匹配「行结尾」,行结尾定义为「\n 之前或 \r 之前」;Python 的 $ 类似。跨语言迁移正则时,对「行尾」的定义要单独验证。


17. 调试方法 ​

17.1 拆解表达式 ​

不要一次写完整复杂表达式。推荐拆成小块:

  1. 先匹配固定前缀。
  2. 再加入第一个字段。
  3. 再加入分隔符。
  4. 再加入第二个字段。
  5. 最后加上开头、结尾锚点。

17.2 给复杂正则加注释 ​

Python:

python
pattern = re.compile(r"""
    ^
    (?P<date>\d{4}-\d{2}-\d{2})
    \s+
    (?P<level>INFO|WARN|ERROR)
    \s+
    (?P<message>.+)
    $
""", re.X)

JavaScript 没有内置 verbose 模式,可以用分段拼接的方式组织复杂表达式:

javascript
const logPattern = new RegExp([
  String.raw`^`,
  String.raw`(?<date>\d{4}-\d{2}-\d{2})`,
  String.raw`\s+`,
  String.raw`(?<level>INFO|WARN|ERROR)`,
  String.raw`\s+`,
  String.raw`(?<message>.+)`,
  String.raw`$`,
].join(""));

实际项目中,如果表达式已经复杂到很难维护,往往应该考虑拆分为多步解析。

17.3 准备测试样例 ​

每个正则至少准备两类样例:

  • 应该匹配的正例。
  • 不应该匹配的反例。

手机号例子:

正例:

text
13800138000
19912345678

反例:

text
23800138000
1380013800
138001380000
1380013800a

17.4 在编辑器中练习 ​

VS Code 搜索支持正则表达式,非常适合练习:

  1. 打开搜索面板。
  2. 启用正则模式。
  3. 写表达式。
  4. 观察高亮结果。
  5. 用替换功能验证捕获组。

18. 实战案例 ​

18.1 批量提取接口路径 ​

文本:

text
GET /api/users
POST /api/orders
DELETE /api/orders/1024

表达式:

regex
^(GET|POST|PUT|PATCH|DELETE)\s+(/[^\s]+)$

JavaScript:

javascript
const text = `
GET /api/users
POST /api/orders
DELETE /api/orders/1024
`;

const routes = [...text.matchAll(/^(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s]+)$/gm)]
  .map(item => ({ method: item[1], path: item[2] }));

console.log(routes);

18.2 把下划线命名转成驼峰 ​

javascript
function toCamelCase(input) {
  return input.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
}

console.log(toCamelCase("user_name")); // userName

18.3 规范化日志空白 ​

javascript
const line = "2026-06-30    INFO     user login";
const normalized = line.replace(/\s+/g, " ");
console.log(normalized);

18.4 提取 query 参数 ​

简单场景:

regex
[?&]([^=&]+)=([^&]*)

JavaScript:

javascript
const url = "/search?q=regex&page=1";
const params = Object.fromEntries(
  [...url.matchAll(/[?&]([^=&]+)=([^&]*)/g)]
    .map(([, key, value]) => [decodeURIComponent(key), decodeURIComponent(value)])
);

console.log(params); // { q: "regex", page: "1" }

工程中更推荐:

javascript
const params = new URLSearchParams("q=regex&page=1");
console.log(params.get("q"));

18.5 校验密码复杂度 ​

要求:

  • 8 到 32 位。
  • 至少一个大写字母。
  • 至少一个小写字母。
  • 至少一个数字。
  • 至少一个特殊字符。

表达式:

regex
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\w\s]).{8,32}$

说明:

  • 多个 (?=...) 是前瞻,用来检查条件。
  • 最后的 .{8,32} 控制总长度。

工程建议:复杂密码规则也可以拆成多条可读性更好的判断,并给用户更明确的错误提示。


19. 常见坑 ​

19.1 忘记整体锚定 ​

regex
\d{6}

会匹配 abc123456xyz 中的 123456。

如果要校验整个字符串,应写:

regex
^\d{6}$

19.2 把 . 当成普通点号 ​

regex
1.2

会匹配 1a2、1-2。如果要匹配小数点:

regex
1\.2

19.3 误用贪婪匹配 ​

regex
<.*>

可能一次吞掉多个标签。简单场景可以改成:

regex
<[^>]*>

19.4 用正则解析 HTML ​

简单提取可以临时使用正则,但 HTML 有嵌套、属性转义、注释、脚本、大小写等问题。生产代码应使用 DOMParser、Cheerio、BeautifulSoup、Jsoup 等解析器。

19.5 忽略 Unicode ​

\w、\b、大小写忽略、字符范围在不同语言中的 Unicode 行为并不完全相同。处理中文、emoji、国际化邮箱、非英文用户名时,必须单独测试。

19.6 全局正则状态问题 ​

JavaScript 中带 g 或 y 的正则对象会维护 lastIndex:

javascript
const re = /\d/g;
console.log(re.test("1")); // true
console.log(re.test("1")); // false,lastIndex 已变化

避免方式:

  • 不需要全局时不要加 g。
  • 每次新建正则。
  • 手动重置 re.lastIndex = 0。

20. 练习题 ​

20.1 基础练习 ​

  1. 写一个表达式,匹配 3 到 8 位数字。
  2. 写一个表达式,匹配 .jpg、.jpeg、.png 后缀。
  3. 写一个表达式,匹配以 http:// 或 https:// 开头的字符串。
  4. 写一个表达式,提取 name=Tom age=18 中的姓名和年龄。
  5. 写一个表达式,把多个连续空白替换成一个空格。

参考答案:

regex
^\d{3,8}$
\.(?:jpg|jpeg|png)$
^https?:\/\/
name=(\w+)\s+age=(\d+)
\s+

20.2 进阶练习 ​

  1. 匹配简单的年月日格式 YYYY-MM-DD。
  2. 匹配由相同引号包裹的字符串。
  3. 提取日志里的时间、级别、消息。
  4. 写一个正则,匹配不包含换行和双引号的双引号字符串。
  5. 找出一个可能产生灾难性回溯的表达式,并改写它。

参考答案:

regex
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
(['"]).*?\1
^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(INFO|WARN|ERROR)\s+(.+)$
"[^"\n]*"

灾难性回溯示例:

regex
^(a+)+$

可根据业务改写成更具体的形式,例如只需要判断全是 a:

regex
^a+$

21. 速查表 ​

目标表达式
数字\d
非数字\D
单词字符\w
空白\s
任意字符.
0 次或多次*
1 次或多次+
可选?
指定次数{m,n}
开头^
结尾$
单词边界\b
字符类[abc]
排除字符类[^abc]
分组(abc)
非捕获分组(?:abc)
或`a
正向前瞻(?=...)
负向前瞻(?!...)
正向后顾(?<=...)
负向后顾(?<!...)

22. 学习总结 ​

正则表达式的核心不是背诵所有符号,而是建立“位置 + 字符 + 次数 + 分组 + 条件”的组合思维。

学习时要抓住五个原则:

  1. 校验整个字符串时一定考虑 ^ 和 $。
  2. 提取字段时优先使用分组,复杂结果使用命名捕获。
  3. 能写具体范围就不要滥用 .*。
  4. 不同语言的正则语法和替换语法不完全一致,迁移时必须测试。
  5. 复杂、嵌套、语义化文本不要强行用正则解析,使用专业解析器更可靠。

推荐后续学习顺序:

  1. 在 JavaScript 文档中复习字符串方法:match、matchAll、replace、split。
  2. 在 Python 文档中练习 re.search、re.findall、re.sub。
  3. 在 Java 文档中练习 Pattern 与 Matcher。
  4. 在 VS Code 中使用正则搜索重构一组文本。
  5. 为每个业务正则补充正例、反例和性能边界测试。

基于 VitePress 构建的个人学习文档知识库