Loading...

正则表达式终极教程:从入门到精通,详细到不行

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 优先级

正则优先级从高到低大致是:

  1. 转义 \
  2. 字符类 [...]
  3. 分组 (...)
  4. 量词 * + ? {n,m}
  5. 连接:ab
  6. 分支:|

所以:

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 到 z

5.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+)b

8.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/u

Python:

re.I
re.M
re.S
re.X
re.A
re.U

Java:

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}   Emoji

JavaScript 需要 u 标志:

/\p{Script=Han}+/u

Python re 默认不支持 \p{},可用 regex 模块。

Java 支持:

\p{IsHan}

15.2 中文常用匹配

^[\u4e00-\u9fa5]+$

或:

^\p{Script=Han}+$

15.3 代理对

Emoji 等字符可能占两个 UTF-16 码元。JavaScript 中建议使用 u 标志:

/😀/u


16. 不同语言差异

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\b

17.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 正则测试

调试方法:

  1. 先写最小匹配
  2. 逐步增加约束
  3. 使用分组和注释
  4. 测试正常、边界、异常输入
  5. 检查性能


19. 最佳实践

  1. 明确需求:验证、提取还是替换?
  2. 能不用正则就不用。
  3. 不要解析 HTML、XML、JSON。
  4. 尽量锚定 ^...$。
  5. 使用非捕获组 (?:...)。
  6. 避免嵌套量词。
  7. 避免灾难性回溯。
  8. 使用具体字符类。
  9. 考虑 Unicode。
  10. 测试边界:空字符串、超长字符串、特殊字符。
  11. 预编译正则。
  12. 给复杂正则写注释。
  13. 不同语言语法有差异。
  14. 使用工具验证。
  15. 保持可读性,复杂正则拆成多个简单正则。


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. 用工具测试;
  4. 增加边界条件;
  5. 优化性能和可读性。

如果你把上面的内容拆成文章,可以这样排:

  • 第 1 篇:基础语法与元字符
  • 第 2 篇:分组、捕获、断言、反向引用
  • 第 3 篇:性能、Unicode、语言差异
  • 第 4 篇:实战案例与速查表

这样既详细,又适合读者分阶段学习。

0

回到顶部