外观
正则表达式系统学习文档
资料核对日期: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. 学习路线
建议按下面顺序学习:
- 先理解“普通字符”和“元字符”的区别。
- 掌握字符类:
\d、\w、\s、[...]、[^...]。 - 掌握量词:
*、+、?、{m,n}。 - 掌握锚点与边界:
^、$、\b。 - 掌握分组、捕获、命名捕获、反向引用。
- 掌握贪婪、惰性与回溯。
- 掌握常用断言:前瞻、后顾。
- 学会在 JavaScript、Python、Java 中调用正则。
- 学会用正则做提取、替换、校验、切分。
- 理解性能风险,避免灾难性回溯和 ReDoS。
3. 快速入门
3.1 第一个正则
正则表达式:
regex
hello可以匹配字符串中的 hello。
JavaScript 示例:
javascript
const text = "hello regex";
console.log(/hello/.test(text)); // truePython 示例:
python
import re
text = "hello regex"
print(bool(re.search(r"hello", text))) # TrueJava 示例:
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
regexcrate、Goregexp:用线性时间引擎,根本不需要原子分组。
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")); // false8.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]); // 30Python:
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)) # 309.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); // 2026Python:
python
match = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "2026-06-30")
print(match.group("year")) # 2026Java:
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 catI like catsI like dogI like dogs
10.2 优先级
正则表达式大致优先级:
- 转义与字符。
- 字符类。
- 量词。
- 连接。
- 或
|。
示例:
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 | 多行模式,影响 ^ 和 $ |
s | dotAll,让 . 可匹配换行 |
u | Unicode 模式 |
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_CASE | Unicode 大小写相关增强 |
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 差异速览
| 能力 | JavaScript | Python re | Java java.util.regex |
|---|---|---|---|
| 正则字面量 | 支持 /.../ | 不支持 | 不支持 |
| 原始字符串 | 不支持,常用字面量规避 | 推荐 r"..." | 不支持 |
| 命名捕获写法 | (?<name>...) | (?P<name>...) | (?<name>...) |
| 全局匹配 | g 标志、matchAll | findall、finditer | Matcher.find() 循环 |
| 替换引用 | $1、$<name> | \1、\g<name> | $1、${name} |
| 多行模式 | m | re.M | Pattern.MULTILINE |
| dotAll | s | re.S | Pattern.DOTALL |
| Unicode | u、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 优化原则
- 能具体就不要宽泛:用
[^\n]*代替不必要的.*。 - 能限制长度就限制长度:例如
{1,64}。 - 避免嵌套可变长度量词。
- 把最容易失败的条件放前面。
- 对用户输入设置最大长度。
- 高频正则提前编译复用。
- 复杂文本优先使用解析器,而不是正则硬扛。
示例:匹配双引号字符串。
不够稳的写法:
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 拆解表达式
不要一次写完整复杂表达式。推荐拆成小块:
- 先匹配固定前缀。
- 再加入第一个字段。
- 再加入分隔符。
- 再加入第二个字段。
- 最后加上开头、结尾锚点。
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
1380013800a17.4 在编辑器中练习
VS Code 搜索支持正则表达式,非常适合练习:
- 打开搜索面板。
- 启用正则模式。
- 写表达式。
- 观察高亮结果。
- 用替换功能验证捕获组。
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")); // userName18.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\.219.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 基础练习
- 写一个表达式,匹配 3 到 8 位数字。
- 写一个表达式,匹配
.jpg、.jpeg、.png后缀。 - 写一个表达式,匹配以
http://或https://开头的字符串。 - 写一个表达式,提取
name=Tom age=18中的姓名和年龄。 - 写一个表达式,把多个连续空白替换成一个空格。
参考答案:
regex
^\d{3,8}$
\.(?:jpg|jpeg|png)$
^https?:\/\/
name=(\w+)\s+age=(\d+)
\s+20.2 进阶练习
- 匹配简单的年月日格式
YYYY-MM-DD。 - 匹配由相同引号包裹的字符串。
- 提取日志里的时间、级别、消息。
- 写一个正则,匹配不包含换行和双引号的双引号字符串。
- 找出一个可能产生灾难性回溯的表达式,并改写它。
参考答案:
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. 学习总结
正则表达式的核心不是背诵所有符号,而是建立“位置 + 字符 + 次数 + 分组 + 条件”的组合思维。
学习时要抓住五个原则:
- 校验整个字符串时一定考虑
^和$。 - 提取字段时优先使用分组,复杂结果使用命名捕获。
- 能写具体范围就不要滥用
.*。 - 不同语言的正则语法和替换语法不完全一致,迁移时必须测试。
- 复杂、嵌套、语义化文本不要强行用正则解析,使用专业解析器更可靠。
推荐后续学习顺序:
- 在 JavaScript 文档中复习字符串方法:
match、matchAll、replace、split。 - 在 Python 文档中练习
re.search、re.findall、re.sub。 - 在 Java 文档中练习
Pattern与Matcher。 - 在 VS Code 中使用正则搜索重构一组文本。
- 为每个业务正则补充正例、反例和性能边界测试。