正则表达式终极教程:从入门到精通,详细到不行
1. 正则表达式是什么
正则表达式,英文 Regular Expression,简称 Regex,是一种用于描述字符串模式的微型语言。
它主要用来:
- 验证:邮箱、手机号、密码、日期、身份证
- 查找:从文本中提取 URL、IP、电话、日志字段
- 替换:批量修改格式
- 分割:按复杂规则拆分字符串
- 过滤:筛选日志、代码、数据
例如:
^\d{4}-\d{2}-\d{2}$表示:从开头到结尾,必须是“4 位数字 - 2 位数字 - 2 位数字”,也就是 2025-01-01 这种日期格式。
正则表达式不是万能的。它不适合解析 HTML、XML、JSON 等嵌套结构。复杂结构应使用专门解析器。
2. 学习前必须理解的 6 个概念
2.1 模式
正则表达式本身是一个“模式”,不是普通字符串。
cat表示匹配连续的 cat。
2.2 匹配
匹配可以指:
- 包含:字符串中是否有这个模式
- 完全匹配:整个字符串是否等于这个模式
- 捕获:把匹配到的部分提取出来
例如:
cat在 "I have a cat" 中能匹配到 cat,但不是完全匹配。
2.3 元字符
有特殊含义的字符叫元字符:
. ^ $ * + ? { } [ ] \ | ( )如果要匹配它们本身,通常要转义:
\.
\*
\(
\)2.4 转义
反斜杠 \ 用于:
- 把元字符变普通字符:\. 匹配点号
- 把普通字符变特殊含义:\d 匹配数字
- 表示特殊字符:\n 换行,\t 制表符
2.5 量词
量词表示“前一个元素出现多少次”。
a*
a+
a?
a{3}
a{2,5}2.6 优先级
正则优先级从高到低大致是:
- 转义 \
- 字符类 [...]
- 分组 (...)
- 量词 * + ? {n,m}
- 连接:ab
- 分支:|
所以:
ab|cd表示 ab 或 cd,不是 a 后跟 b 或 c 后跟 d。
3. 元字符总览
| 元字符 | 含义 |
|---|---|
. | 匹配任意字符,默认不匹配换行 |
^ | 行首或字符串开头 |
$ | 行尾或字符串结尾 |
* | 0 次或多次 |
+ | 1 次或多次 |
? | 0 次或 1 次;也可表示懒惰 |
{n} | 恰好 n 次 |
{n,} | 至少 n 次 |
{n,m} | n 到 m 次 |
[...] | 字符类 |
[^...] | 否定字符类 |
| | 或 |
(...) | 分组并捕获 |
(?:...) | 非捕获分组 |
\ | 转义 |
\d | 数字 |
\D | 非数字 |
\w | 单词字符 |
\W | 非单词字符 |
\s | 空白 |
\S | 非空白 |
\b | 单词边界 |
\B | 非单词边界 |
4. 字符与转义
4.1 普通字符
普通字符就是字面量:
regex
abc
123
你好4.2 特殊字符转义
这些字符有特殊含义:
. ^ $ * + ? { } [ ] \ | ( )匹配它们本身时:
\.
\^
\$
\*
\+
\?
\{
\}
\[
\]
\\
\|
\(
\)示例:匹配 example.com:
example\.com因为 . 默认匹配任意字符,\. 才匹配真正的点。
4.3 常见转义序列
\n 换行
\r 回车
\t 制表符
\f 换页
\v 垂直制表符
\0 空字符
\x41 十六进制 41,即 A
\u4E2D Unicode 字符 中
\cA 控制字符 Ctrl+A不同语言对 \u、\x{} 支持不同。
5. 字符类
字符类用 [] 表示“匹配其中任意一个字符”。
[abc]匹配 a、b 或 c。
[0-9]匹配任意一个数字。
[a-z]匹配任意一个小写字母。
[A-Za-z0-9_]匹配字母、数字、下划线。
5.1 范围
[a-z]
[A-Z]
[0-9]
[a-zA-Z0-9]5.2 否定字符类
[^abc]匹配不是 a、b、c 的任意字符。
[^0-9]匹配非数字。
5.3 字符类中的元字符规则
在 [] 内,大部分元字符会失去特殊含义:
[. * + ? ( ) { }]这些都按字面量处理。
但要注意:
- ] 需要转义:\]
- \ 需要转义:\\
- ^ 在第一个位置表示否定
- - 在中间表示范围,在开头或结尾表示普通连字符
示例:
[a-z] a 到 z
[-a-z] 连字符、a 到 z
[a-z-] a 到 z、连字符
[^a-z] 非 a 到 z5.4 POSIX 字符类
在某些语言和工具中支持:
[[:alpha:]] 字母
[[:digit:]] 数字
[[:alnum:]] 字母数字
[[:space:]] 空白
[[:upper:]] 大写字母
[[:lower:]] 小写字母
[[:punct:]] 标点
[[:xdigit:]] 十六进制数字6. 预定义字符类
6.1 \d 与 \D
\d 数字,通常等价 [0-9]
\D 非数字示例:
\d{3}匹配 3 位数字。
6.2 \w 与 \W
\w 单词字符,通常 [A-Za-z0-9_]
\W 非单词字符注意:在 Unicode 模式下,某些语言中 \w 可能匹配中文、日文等字母。
6.3 \s 与 \S
\s 空白字符:空格、制表符、换行等
\S 非空白字符常见等价:
\s ≈ [ \t\n\r\f\v]6.4 点号 .
.默认匹配除换行外的任意字符。
如果开启 s 或 DOTALL 标志,. 也匹配换行。
7. 量词:贪婪、懒惰、占有
7.1 基本量词
a* 0 次或多次
a+ 1 次或多次
a? 0 次或 1 次
a{3} 恰好 3 次
a{3,} 至少 3 次
a{2,5} 2 到 5 次量词只作用于前一个元素:
ab+ 表示 a 后面跟 1 个或多个 b
(ab)+ 表示 ab 重复 1 次或多次7.2 贪婪模式
默认是贪婪的,尽可能多匹配。
<.*>对:
<div>hello</div>会匹配整个 <div>hello</div>。
7.3 懒惰模式
在量词后加 ?:
<.*?>会尽可能少匹配,匹配 <div>。
常见懒惰量词:
*?
+?
??
{n,m}?7.4 占有量词
部分引擎支持:
*+
++
?+
{n,m}+占有量词不回溯,性能更好,但匹配更严格。
例如:
".*+"不会为了匹配后面的内容而回退。
7.5 贪婪与懒惰对比
a.*b在 a1b2b3b 中匹配 a1b2b3b。
a.*?b在 a1b2b3b 中匹配 a1b。
8. 分组与捕获
8.1 捕获分组
(abc)括号把 abc 作为一个整体,并捕获内容。
示例:
(\d{4})-(\d{2})-(\d{2})匹配 2025-01-01,捕获:
- 第 1 组:2025
- 第 2 组:01
- 第 3 组:01
8.2 非捕获分组
(?:abc)只分组,不捕获。
示例:
(?:https?|ftp)://匹配 http://、https://、ftp://,但不单独捕获协议。
8.3 命名捕获组
不同语言语法不同:
(?<year>\d{4}) JavaScript、.NET、Java、PCRE
(?P<year>\d{4}) Python、PCRE
(?'year'\d{4}) .NET、PCRE示例:
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})8.4 分支重置
PCRE 等支持:
(?|(a)|(b))让不同分支的编号从同一位置开始。
8.5 原子组
(?>...)原子组一旦匹配成功,就不回溯。
示例:
(?>a+)b8.6 条件组
PCRE、.NET 等支持:
(?(1)yes|no)如果第 1 组匹配过,则匹配 yes,否则匹配 no。
8.7 递归
PCRE、Perl 等支持:
(?R)
(?0)
(?1)用于匹配嵌套结构,但复杂且易出错。
9. 锚点与边界
9.1 ^ 和 $
^abc
abc$
^abc$默认:
- ^ 匹配字符串开头
- $ 匹配字符串结尾
多行模式下:
- ^ 匹配每一行开头
- $ 匹配每一行结尾
9.2 \A、\Z、\z
\A 字符串开头
\Z 字符串结尾或末尾换行前
\z 绝对字符串结尾不同语言支持不同。
9.3 \b 和 \B
\b 单词边界
\B 非单词边界示例:
\bcat\b匹配单独的 cat,不匹配 category。
\Bcat\B匹配 category 中的 cat 这种非边界情况。
9.4 \G
匹配上一次匹配结束的位置,常用于全局连续匹配。
10. 零宽断言
断言不消耗字符,只判断位置。
10.1 正先行断言
(?=...)后面必须是什么。
示例:匹配后面跟着数字的字母:
[a-z]+(?=\d)10.2 负先行断言
(?!...)后面不能是什么。
示例:匹配后面不是数字的字母:
[a-z]+(?!\d)10.3 正后行断言
(?<=...)前面必须是什么。
示例:匹配前面是 $ 的数字:
(?<=\$)\d+10.4 负后行断言
(?<!...)前面不能是什么。
示例:匹配前面不是 $ 的数字:
(?<!\$)\d+注意:部分引擎要求后行断言长度固定,部分支持变长。使用前确认目标语言。
10.5 断言组合示例
密码必须包含数字:
(?=.*\d)密码必须包含小写、大写、数字:
(?=.*[a-z])(?=.*[A-Z])(?=.*\d)11. 反向引用
反向引用用于引用前面捕获组匹配到的内容。
(\w+)\s+\1匹配重复单词,例如 hello hello。
命名反向引用:
(?<word>\w+)\s+\k<word>Python:
(?P<word>\w+)\s+(?P=word)示例:匹配成对标签:
<([a-z]+)>.*?</\1>注意:这不能可靠解析 HTML,只适合简单场景。
12. 修饰符 / 标志
常见标志:
i 忽略大小写
g 全局匹配
m 多行模式
s 点号匹配换行
u Unicode 模式
y sticky,从 lastIndex 开始
x 扩展模式,忽略空白和注释
A 仅字符串开头
D $ 仅匹配字符串末尾
U 交换贪婪与懒惰
J 允许重复命名组
d 返回索引
v Unicode 集合模式JavaScript 示例:
/abc/i
/abc/g
/abc/m
/abc/s
/abc/uPython:
re.I
re.M
re.S
re.X
re.A
re.UJava:
Pattern.CASE_INSENSITIVE
Pattern.MULTILINE
Pattern.DOTALL
Pattern.COMMENTS
Pattern.UNICODE_CASE.NET:
RegexOptions.IgnoreCase
RegexOptions.Multiline
RegexOptions.Singleline
RegexOptions.IgnorePatternWhitespace
RegexOptions.Compiled扩展模式 x 示例:
(?x)
\d{4} # 年
-
\d{2} # 月
-
\d{2} # 日13. 替换与分割
13.1 JavaScript
str.replace(/cat/g, 'dog')
str.replace(/(\w+), (\w+)/, '$2 $1')特殊替换:
$1 第 1 个捕获组
$& 整个匹配
$` 匹配前的内容
$' 匹配后的内容
$$ 美元符号
$<name> 命名捕获组13.2 Python
re.sub(r'cat', 'dog', text)
re.sub(r'(\w+), (\w+)', r'\2 \1', text)
re.sub(r'(?P<first>\w+), (?P<second>\w+)', r'\g<second> \g<first>', text)13.3 Java
str.replaceAll("cat", "dog");
str.replaceAll("(\\w+), (\\w+)", "$2 $1");13.4 分割
"a,b,c".split(/,/)
"a1b2c".split(/\d/)注意:某些语言中,分割正则里的捕获组会出现在结果中。
14. 正则引擎与性能
14.1 DFA 与 NFA
- DFA:文本导向,线性时间,不支持反向引用和复杂捕获。
- NFA:表达式导向,支持回溯、捕获、断言,但可能慢。
- 大多数语言使用 NFA 或混合引擎。
14.2 回溯
正则引擎会尝试所有可能路径。
例如:
a.*b在长文本中,.* 会先吃到结尾,然后回退找 b。
14.3 灾难性回溯
危险模式:
^(a+)+$输入:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaX可能导致指数级回溯。
避免:
^a+$或使用原子组:
^(?>a+)$或占有量词:
^a++$14.4 性能建议
- 尽量锚定:^...$
- 使用具体字符类代替 .
- 避免嵌套量词:(a+)+
- 避免 .* 到处飞
- 使用非捕获组 (?:...)
- 限制输入长度
- 预编译正则
- 使用工具测试最坏情况
- 对复杂文本使用解析器而不是正则
15. Unicode 与中文
15.1 Unicode 属性
\p{L} 字母
\p{N} 数字
\p{Lu} 大写字母
\p{Ll} 小写字母
\p{Script=Han} 汉字
\p{Emoji} EmojiJavaScript 需要 u 标志:
/\p{Script=Han}+/uPython re 默认不支持 \p{},可用 regex 模块。
Java 支持:
\p{IsHan}15.2 中文常用匹配
^[\u4e00-\u9fa5]+$或:
^\p{Script=Han}+$15.3 代理对
Emoji 等字符可能占两个 UTF-16 码元。JavaScript 中建议使用 u 标志:
/😀/u16. 不同语言差异
16.1 JavaScript
const re = /abc/g;
re.test(str);
str.match(re);
str.matchAll(re);
str.replace(re, 'x');
str.split(re);特点:
- 支持命名组 (?<name>...)
- 支持后行断言
- 支持 u、y、s、d、v
- 不支持原子组、占有量词
- g 标志会影响 lastIndex
16.2 Python
import re
re.search(r'abc', text)
re.match(r'abc', text)
re.fullmatch(r'abc', text)
re.findall(r'\d+', text)
re.finditer(r'\d+', text)
re.sub(r'cat', 'dog', text)
re.split(r',', text)特点:
- 命名组 (?P<name>...)
- 反向引用 (?P=name)
- 默认 Unicode
- re.ASCII 限制为 ASCII
- 不支持 \p{},除非用 regex
16.3 Java
Pattern p = Pattern.compile("\\d+");
Matcher m = p.matcher(text);
while (m.find()) {
System.out.println(m.group());
}特点:
- 支持占有量词
- 支持原子组
- 支持 \Q...\E 字面量
- \d 默认通常为 ASCII,可开启 Unicode 类
16.4 .NET
特点:
- 支持命名组 (?<name>...)、(?'name'...)
- 支持平衡组
- 支持变长后行
- 支持条件组
16.5 PCRE / PHP
特点:
- 支持递归 (?R)
- 支持原子组、占有量词
- 支持条件组
- PHP 使用 preg_match、preg_replace
16.6 Go、Rust
- Go RE2:线性时间,不支持反向引用、环视
- Rust regex:线性时间,不支持反向引用、环视
- 安全,但功能少
16.7 grep、sed、awk
- BRE:基本正则,+ ? | 常需转义
- ERE:扩展正则,支持 + ? |
- grep -E、sed -E、awk 各有差异
17. 常用正则示例
17.1 邮箱
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$说明:实用但不完全符合 RFC。真正验证邮箱应发确认邮件。
17.2 UR
^https?://[^\s/$.?#].[^\s]*$简化版,适合一般提取。
17.3 IPv4
^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$17.4 日期 YYYY-MM-DD
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$17.5 时间 HH:MM:SS
^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$17.6 中国手机号
^1[3-9]\d{9}$17.7 密码强度
至少 8 位,含小写、大写、数字:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$再加特殊字符:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$17.8 十六进制颜色
^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$17.9 身份证
^\d{17}[\dXx]$17.10 邮政编码
^\d{6}$17.11 提取 Markdown 链接
\[([^\]]+)\]\(([^)]+)\)17.12 重复单词
\b(\w+)\s+\1\b17.13 删除首尾空白
^\s+|\s+$17.14 匹配 HTML 标签
<([a-zA-Z][\w-]*)(?:\s+[^>]*)?>(.*?)</\1>警告:不要用正则解析 HTML。
17.15 日志行
^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) \[(\w+)\] (.*)$18. 调试工具
- regex101:功能最全,支持多语言、解释、调试
- RegExr:适合学习和测试
- Debuggex:可视化铁路图
- Regexper:生成正则结构图
- PyRegex:Python 正则测试
- Rubular:Ruby 正则测试
调试方法:
- 先写最小匹配
- 逐步增加约束
- 使用分组和注释
- 测试正常、边界、异常输入
- 检查性能
19. 最佳实践
- 明确需求:验证、提取还是替换?
- 能不用正则就不用。
- 不要解析 HTML、XML、JSON。
- 尽量锚定 ^...$。
- 使用非捕获组 (?:...)。
- 避免嵌套量词。
- 避免灾难性回溯。
- 使用具体字符类。
- 考虑 Unicode。
- 测试边界:空字符串、超长字符串、特殊字符。
- 预编译正则。
- 给复杂正则写注释。
- 不同语言语法有差异。
- 使用工具验证。
- 保持可读性,复杂正则拆成多个简单正则。
20. 练习题与答案
练习 1
匹配 24 小时时间 23:59:59。
答案:
^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$练习 2
提取所有邮箱。
答案:
[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}练习 3
匹配至少 8 位,包含数字和字母的密码。
答案:
^(?=.*[A-Za-z])(?=.*\d).{8,}$练习 4
匹配 HTML 注释。
答案:
<!--.*?-->需要 s 标志支持跨行。
练习 5
把 2025-01-01 替换成 01/01/2025。
JavaScript:
str.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1')Python:
re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\3/\2/\1', text)21. 速查表
. 任意字符,默认不含换行
^ 行首/字符串首
$ 行尾/字符串尾
* 0 次或多次
+ 1 次或多次
? 0 次或 1 次
{n} 恰好 n 次
{n,} 至少 n 次
{n,m} n 到 m 次
*? 懒惰 0 次或多次
+? 懒惰 1 次或多次
?? 懒惰 0 或 1 次
[...] 字符类
[^...] 否定字符类
\d 数字
\D 非数字
\w 单词字符
\W 非单词字符
\s 空白
\S 非空白
\b 单词边界
\B 非单词边界
\A 字符串开头
\Z 字符串结尾或末尾换行前
\z 绝对字符串结尾
\G 上次匹配结束位置
(...) 捕获组
(?:...) 非捕获组
(?<name>...) 命名捕获组
\1 反向引用第 1 组
(?=...) 正先行
(?!...) 负先行
(?<=...) 正后行
(?<!...) 负后行
(?>...) 原子组
*+ ++ ?+ 占有量词
i 忽略大小写
g 全局
m 多行
s 点号匹配换行
u Unicode
x 扩展模式结语
正则表达式是一门“写起来短,学起来深”的技术。入门只需要记住元字符、字符类、量词、分组、锚点;进阶要理解断言、反向引用、引擎回溯;实战则要结合具体语言、Unicode、性能和可读性。
最有效的学习方式不是背,而是:
- 明确要匹配什么;
- 写出最小正则;
- 用工具测试;
- 增加边界条件;
- 优化性能和可读性。
如果你把上面的内容拆成文章,可以这样排:
- 第 1 篇:基础语法与元字符
- 第 2 篇:分组、捕获、断言、反向引用
- 第 3 篇:性能、Unicode、语言差异
- 第 4 篇:实战案例与速查表
这样既详细,又适合读者分阶段学习。