正则表达式(Regex)完整指南
# 🔍 正则表达式完整指南
适合人群:程序员、数据分析师、文本处理工程师
学习时间:2-3小时
前置知识:基础编程概念
正则表达式(Regular Expression,简称regex)是一种强大的文本模式匹配工具,广泛应用于字符串处理、数据验证、文本搜索等领域。本文从基础语法讲到性能优化,配合实战案例,帮你系统掌握正则表达式。
# 📖 目录
# 🔤 基础语法
# 1. 基本字符
| 字符 | 说明 | 示例 |
|---|---|---|
. | 除换行符以外的所有字符 | a.c 匹配 "abc", "a1c" |
^ | 字符串开头(在 [...] 内首位则表示取反,见「字符集合」) | ^hello 匹配以"hello"开头的字符串 |
$ | 字符串结尾 | world$ 匹配以"world"结尾的字符串 |
\b | 单词边界(\w 与非 \w 的交界处) | \bcat\b 精确匹配单词 "cat",不匹配 "category" |
\ | 转义字符 | \. 匹配字面意思的点号 |
# 2. 字符类
| 字符类 | 说明 | 示例 |
|---|---|---|
\d | 匹配数字 (0-9) | \d+ 匹配一个或多个数字 |
\w | 匹配字母、数字、下划线 | \w+ 匹配一个或多个单词字符 |
\s | 匹配空白字符 | \s+ 匹配一个或多个空格 |
\D | 匹配非数字 | \D+ 匹配一个或多个非数字 |
\W | 匹配非单词字符 | \W+ 匹配一个或多个非单词字符 |
\S | 匹配非空白字符 | \S+ 匹配一个或多个非空白字符 |
注意:
\d\w\s等转义是 PCRE 方言特性,POSIX 工具(sed/grep/awk)不认,等价写法见下文「正则方言」一节。
# 3. 字符集合
| 模式 | 说明 | 示例 |
|---|---|---|
[abc] | 匹配 a、b 或 c 中的一个字母 | [abc] 匹配 "a", "b", "c" |
[a-z] | 匹配 a 到 z 中的一个字母 | [a-z]+ 匹配小写字母 |
[A-Z] | 匹配 A 到 Z 中的一个字母 | [A-Z]+ 匹配大写字母 |
[0-9] | 匹配 0 到 9 中的一个数字 | [0-9]+ 匹配数字 |
[^abc] | 匹配 a、b、c 之外的任意一个字符(^ 在类内首位 = 取反) | [^abc] 匹配除 abc 外的任意字符 |
[\u4e00-\u9fa5] | 匹配中文字符 | [\u4e00-\u9fa5]+ 匹配中文 |
[^\x00-\x7F] | 匹配非 ASCII 字符(含中文) | [^\x00-\x7F]+ 匹配连续中文 |
\p{...} | Unicode 属性类(需 u 修饰符) | \p{Emoji} 匹配 emoji |
^的位置决定含义(高频困惑点):
位置 含义 示例 类外 锚点:行/字符串开头 ^abc类内第一位 取反:匹配所列字符之外的任意一个字符 [^abc]类内非首位 字面量:匹配 ^本身[a^]、[\^]
# 4. 量词
| 量词 | 说明 | 示例 |
|---|---|---|
? | 0 次或 1 次匹配 | colou?r 匹配 "color" 或 "colour" |
* | 匹配 0 次或多次 | runoo*b 匹配 "runob", "runoob", "runooob" |
+ | 匹配 1 次或多次 | runoo+b 匹配 "runoob", "runooob" |
{n} | 匹配 n 次 | \d{3} 匹配恰好3个数字 |
{n,} | 匹配 n 次以上 | \d{3,} 匹配3个或更多数字 |
{m,n} | 最少 m 次, 最多 n 次匹配 | \d{3,5} 匹配3到5个数字 |
# 5. 分组和引用
| 模式 | 说明 | 示例 |
|---|---|---|
(expr) | 捕获分组, 可通过 \1 或 $1 引用 | (\w+)\s\1 匹配重复的单词 |
(?:expr) | 非捕获分组, 不占用分组编号 | (?:abc\|def) 匹配但不捕获 |
(?<name>expr) | 命名捕获分组, 通过名称引用 | (?<year>\d{4}) 用 year 引用 |
\1 与 $1 的区别:两者都引用捕获组,但用在不同位置——
\1:写在模式内部,引用前面捕获组匹配到的内容(反向引用),如(\w+)\s\1匹配 "hello hello" 这类重复单词$1:写在替换文本中,把捕获组的内容插入替换结果,如str.replace(/(\w+) (\w+)/, '$2 $1')交换两个单词
命名分组的引用方式:模式内用 \k<name>,替换时 JavaScript 用 $<name>,其他语言多为 ${name}。
# 捕获分组和非捕获分组的区别
理解非捕获分组的关键在于了解正则表达式中捕获分组和非捕获分组的区别
捕获分组和非捕获分组的区别
- 捕获分组: 用圆括号
()包围的部分,匹配的内容会被捕获,可以通过反向引用或者程序来提取这些匹配的内容 - 非捕获分组: 用
(?:...)包围的部分,匹配的内容不会被捕获,不会影响分组的编号,也不能通过反向引用来提取内容
举个例子
假设我们有以下字符串和正则表达式:
字符串: "JsApiTransformWork and JsApiTransformTask"
正则表达式: (\w+)(Work|Task)
2
这个正则表达式中包含两个捕获分组:
(\w+)捕获字母或数字的连续字符串(Work|Task)捕获 Work 或 Task
如果你在代码中使用这个正则表达式来匹配字符串,例如在 JavaScript 中:
const regex = /(\w+)(Work|Task)/g;
const str = 'JsApiTransformWork and JsApiTransformTask';
const matches = [...str.matchAll(regex)];
console.log(matches);
2
3
4
5
结果会是这样的:
[
['JsApiTransformWork', 'JsApiTransform', 'Work'],
['JsApiTransformTask', 'JsApiTransform', 'Task'],
];
2
3
4
这里 matches 包含两个匹配结果,每个匹配结果包含三个部分:
- 整个匹配的字符串,如 "JsApiTransformWork"
- 第一个捕获分组的内容,如 "JsApiTransform"
- 第二个捕获分组的内容,如 "Work" 或 "Task"
使用非捕获分组
如果你使用非捕获分组 (?:Work|Task),正则表达式如下:
(\w+)(?:Work|Task)
再次运行上面的代码,结果会变成这样:
[
['JsApiTransformWork', 'JsApiTransform'],
['JsApiTransformTask', 'JsApiTransform'],
];
2
3
4
这里 matches 仍然包含两个匹配结果,但每个匹配结果只有两个部分:
- 整个匹配的字符串,如 "JsApiTransformWork"
- 第一个捕获分组的内容,如 "JsApiTransform"
这次 Work 或 Task 的匹配结果没有被捕获,也没有出现在 matches 数组中
总结
使用 (?:Work|Task) 非捕获分组,意味着我们只想匹配 Work 或 Task,但不需要在匹配结果中提取或引用它们。这样可以简化结果,并且有助于避免不必要的捕获分组
# 引用捕获组索引
了解 $1
正则表达式中的 $1 不是用于匹配文本的模式, 而是用于在正则表达式替换中引用匹配组(捕获组)的特殊语法。它通常用于替换操作, 以便在替换文本中使用匹配的内容
在正则表达式替换中, 通常使用圆括号 () 来创建捕获组, 然后可以使用 $1、$2、$3 等来引用这些捕获组, 以在替换文本中插入匹配的内容。每个 $n 表示引用第 n 个捕获组的内容
例如, 假设我们有以下文本:
Name: John, Age: 30
如果我们想要提取名字和年龄, 并在替换文本中使用它们, 可以使用正则表达式:
Name: (\w+), Age: (\d+)
在替换文本中, 我们可以使用 $1 来引用第一个捕获组(名字), 使用 $2 来引用第二个捕获组(年龄)。例如, 可以将其替换为:
Hello, $1! You are $2 years old.
替换后的文本将是:
Hello, John! You are 30 years old.
请注意, $1 在正则表达式模式中不是一个有效的匹配, 而是在替换操作中用于引用捕获组的方式
# 6. 选择
| 模式 | 说明 | 示例 |
|---|---|---|
aa\|bb | 匹配 aa 或 bb | cat\|dog 匹配 "cat" 或 "dog" |
# 🔍 进阶语法
# 断言
断言(Assertion)只做判断、不消费字符,即匹配结果中不包含断言部分,因此也称为"零宽度"匹配。
| 模式 | 名称 | 说明 | 示例 |
|---|---|---|---|
(?=expr) | 正向先行断言 | 后面必须紧跟 expr | \w+(?=\s) 匹配后面跟空格的单词(不含空格) |
(?!expr) | 负向先行断言 | 后面不能紧跟 expr | \d(?!\d) 匹配后面不再是数字的数字 |
(?<=expr) | 正向后行断言 | 前面必须是 expr | (?<=\$)\d+ 匹配 $ 后面的数字 |
(?<!expr) | 负向后行断言 | 前面不能是 expr | (?<!-)\d+ 匹配前面不是负号的数字 |
注意:部分老引擎(如旧版 Safari 的 JavaScript)不支持后行断言,使用前需确认兼容性。 ripgrep(rg)默认的 Rust regex 引擎更严格:先行/后行断言、反向引用一概不支持(为保证线性匹配时间的设计取舍)。报错
look-around ... is not supported时,加--pcre2换引擎,或用下方案例 4 的字符类等价改写。
断言实战
- 匹配后面不跟空格的冒号:
:(?! )
匹配 : 且后面不是空格,如 key:value 中的 :(而 key: value 中 : 后有空格,不匹配)。
- 排除特定赋值:
colorChangeConfig(?!=undefined)
匹配 colorChangeConfig,且其后不能紧跟 =undefined(用于过滤 colorChangeConfig=undefined 这类无效配置)。
- Next.js 中间件路径排除:
/((?!api|_next/static|_next/image|.*\.png$).*)
((?!...).*): 负向先行断言, 表示后面的表达式不应该匹配某些模式?!api|_next/static|_next/image|.*\.png$: 要排除的模式列表api: 排除路径中包含 api 的情况_next/static: 排除路径中包含 _next/static 的情况_next/image: 排除路径中包含 _next/image 的情况.*\.png$: 排除以 .png 结尾的路径
因此, 这个正则表达式的含义是: 匹配所有不包含 api、_next/static、_next/image 和不以 .png 结尾的路径。换句话说, 中间件将会在这些路径之外的所有路径上执行。
- 断言的等价改写(引擎不支持时的替代方案):
匹配 markdown 链接 [1.2.3](...) 但排除锚点 [1.2.3](#...):
\[\d+\.\S+\]\((?!#) # 负向先行断言版(PCRE/JS/rg --pcre2)
\[\d+\.\S+\]\([^#] # 字符类等价版(rg 默认引擎也可用)
2
当 (?!X) 排除的只是单个字符时,[^X] 是等价替代。两者差异:
- 断言零宽度不消费字符,字符类会消费一个字符 → 影响
-o(only-matching)、--replace场景的输出 X出现在其他位置不受影响:[1.2.3](http://x.com/page#section)两版都匹配,不会被误排- 行尾恰好断在
(后的残缺行只有断言版能匹配,实际文本中可忽略
# 贪婪与懒惰
量词默认是贪婪的,会尽可能多地匹配字符。在量词后加 ? 变为懒惰(非贪婪)模式,尽可能少地匹配。
| 贪婪 | 懒惰 | 说明 |
|---|---|---|
* | *? | 0 次或多次 → 尽可能少匹配 |
+ | +? | 1 次或多次 → 尽可能少匹配 |
? | ?? | 0 次或 1 次 → 尽可能少匹配 |
{m,n} | {m,n}? | m 到 n 次 → 尽可能少匹配 |
案例:一行中有多个 : 时,只想匹配到第一个 : 为止:
^\d\. (.*?):
对 2. aaa: bbb: 而言:
(.*?)懒惰匹配 → 捕获组得到aaa(到第一个:停止)(.*)贪婪匹配 → 捕获组得到aaa: bbb(回溯到最后一个:)
⚠️ 注意:懒惰量词是 PCRE 特性,POSIX 工具(sed/grep/awk)不支持——GNU sed 遇到
.+?不报错但按贪婪处理,BSD sed 直接报错,详见下文「正则方言」一节。
# 修饰符(Flags)
修饰符不改变模式本身,而是改变匹配的行为方式,写在模式之外:
| 修饰符 | 名称 | 说明 |
|---|---|---|
g | global | 全局匹配,找出所有匹配而非第一个 |
i | ignoreCase | 忽略大小写 |
m | multiline | ^ 和 $ 匹配每一行的开头/结尾,而非整个字符串的开头/结尾 |
s | dotAll | . 也匹配换行符 |
u | unicode | 启用 Unicode 模式,支持 \u{...} 和 \p{...} |
y | sticky | 粘性匹配,从 lastIndex 位置开始匹配(仅 JavaScript) |
各语言写法对照:
// JavaScript: /pattern/flags
const re = /^\d+$/gm;
2
# Python: 内联 (?flags) 或函数参数
import re
re.findall(r'^\d+$', text, re.MULTILINE)
re.findall(r'(?m)^\d+$', text) # 内联写法
2
3
4
# 正则方言(BRE / ERE / PCRE)
正则不是一种语言,而是几大"方言"家族,跨工具使用前先确认对方说的是哪一种:
| 方言 | 哪里在用 | 语法特点 |
|---|---|---|
| BRE | sed、grep 的默认模式 | ? + | ( ) 是普通字符;分组写 \(...\);\? \+ \| 是 GNU 私有扩展(BSD 不认) |
| ERE | sed -E、grep -E、awk | ? + | () 直接可用;但 \d \s \w 不属于 POSIX(细节见下方对照表) |
| PCRE | Perl、grep -P;JavaScript、Python、Java 等语言的引擎语法同源 | 本文教的语法基本都源于此;各语言细节差异见 Q1 |
| Rust regex | rg(ripgrep)默认引擎、Rust 语言 | 语法接近 PCRE:\d \w (?:...) +? 均可用;但断言、反向引用一概不支持(线性时间保证),rg --pcre2 可切换到 PCRE2 |
关键提醒:本文基础语法里的 \d \w \s \b、懒惰量词 +?、非捕获分组 (?:...)、断言 (?=) 都是 PCRE 特性,POSIX 工具(sed/grep/awk)一概不认(rg 默认引擎支持前几项,但断言同样不支持,需 --pcre2)。POSIX 等价写法:
| PCRE 写法 | POSIX 等价 | 说明 |
|---|---|---|
\d | [0-9] | 数字 |
\w | [[:alnum:]_] | 单词字符 |
\s | [[:space:]] | 空白 |
\b | 无标准等价 | GNU 认 \b,BSD 认 [[:<:]]/[[:>:]],互不相认 |
+? 懒惰量词 | 无 | 用 [^分隔符] 排除法,或确认要的就是贪婪 .* |
(?:...) 非捕获 | 无 | POSIX 只有捕获分组 |
实战案例:同一脚本本地 GNU sed 能跑、macOS/Xcode 的 BSD sed 直接报错的全过程排查,见 sed 用法与 BSD/GNU 兼容性踩坑。
# 💼 实战案例
以下为文本解析中常用的复合模式(以解析快捷键说明文本为例):
| 正则 | 说明 | 匹配示例 |
|---|---|---|
^\d\.\s[^\x00-\x7F]+: $ | 单数字 + 点 + 空格 + 若干非 ASCII 字符(中文)+ 冒号空格结尾 | 1. 复制: |
(^[^\x00-\x7F]+) ([a-zA-Z]) ([^\x00-\x7F]): | 中文 + 空格 + 单个字母 + 空格 + 中文 + 冒号空格 | 按下 i 键: |
([a-zA-Z.]+) | 连续的字母和点号 | Ctrl.A 中的 Ctrl.A |
(^\w.*): | 以单词字符开头, 匹配到冒号空格(贪婪, 到最后一个) | 2. aaa: bbb: → 2. aaa: bbb |
(^[^\s]+): | 行首连续非空白字符, 到冒号空格 | Ctrl+C: 复制 → Ctrl+C |
# ⚡ 性能优化
正则引擎匹配失败时,会退回到之前的状态尝试其他匹配路径,这个过程叫回溯(backtracking)。嵌套量词(如 (a+)+)会让可选路径数呈指数级增长,导致"灾难性回溯"——匹配耗时呈指数级上升,甚至卡死服务。性能优化的核心就是减少回溯。
# 1. 避免回溯灾难
问题示例:
(a+)+b
当匹配 "aaaaaaaaac" 时,会产生大量回溯。
优化方案:
a+b
# 2. 使用非捕获分组
避免不必要的捕获:
# 不好的做法
(abc|def)
# 好的做法。不产生捕获,节省性能
(?:abc|def)
2
3
4
5
# 3. 使用原子分组
防止回溯:
# 原子分组(某些引擎支持)
(?>a+)+b
2
# 4. 优化字符类
使用更精确的字符类:
# 不好的做法
.*
# 好的做法
[^<]*
2
3
4
5
# ❓ 常见问题
# Q1: 不同正则引擎有什么差异?
A: 正则语法并非完全通用,跨语言使用时注意:
- JavaScript 不支持原子分组
(?>...)和占有优先量词a*+ - Python
re不支持递归匹配,需要第三方regex库 - 后行断言
(?<=)在旧版 Safari(< 16.4)的 JavaScript 中不可用 - 各引擎对
\p{...}的支持程度不同,使用前在对应引擎中测试 - POSIX 工具(sed/grep/awk)用的是 BRE/ERE 方言,
\d\s+?等 PCRE 特性不可用,见上文「正则方言」一节
# Q2: JavaScript 的 lastIndex 有什么坑?
A: 带 g/y 修饰符的正则对象会记住匹配位置(lastIndex),重复使用同一个正则对象时状态会延续:
const re = /\d/g;
re.test('a1'); // true
re.test('a1'); // false!lastIndex 已到末尾,从头之外开始匹配
2
3
每次匹配失败后 lastIndex 会重置为 0,但成功后不会。在循环或函数复用场景中,要么每次新建正则,要么手动 re.lastIndex = 0。
# Q3: 如何匹配多行文本?
A: 使用多行模式标志:
# JavaScript
/^start.*end$/m
# Python
re.MULTILINE
2
3
4
5
# Q4: 如何匹配特殊字符?
A: 使用转义字符:
# 匹配点号
\.
# 匹配方括号
\[
\]
2
3
4
5
6
# Q5: 如何处理复杂的嵌套结构?
A: 对于复杂的嵌套结构,正则表达式可能不是最佳选择:
- 考虑使用专门的解析器
- 或者分步骤处理
- 使用递归正则表达式(如果引擎支持)
# 🛠️ 工具与资源
# 在线工具
Regex101 - https://regex101.com/ (opens new window)
- 支持多种正则表达式引擎
- 实时测试和调试
- 详细的解释和匹配信息
RegExr - https://regexr.com/ (opens new window)
- 交互式学习工具
- 内置常用模式库
- 支持JavaScript正则表达式
RegexPal - https://www.regexpal.com/ (opens new window)
- 简单易用的在线测试工具
- 支持JavaScript正则表达式
# 编程语言工具
# JavaScript
// 测试正则表达式
const regex = /\d{3}-\d{3}-\d{4}/;
const phone = "123-456-7890";
console.log(regex.test(phone)); // true
// 提取匹配内容
const matches = phone.match(regex);
console.log(matches[0]); // "123-456-7890"
2
3
4
5
6
7
8
# Python
import re
# 测试正则表达式
pattern = r'\d{3}-\d{3}-\d{4}'
phone = "123-456-7890"
match = re.search(pattern, phone)
if match:
print(match.group()) # "123-456-7890"
2
3
4
5
6
7
8
# Java
import java.util.regex.Pattern;
import java.util.regex.Matcher;
Pattern pattern = Pattern.compile("\\d{3}-\\d{3}-\\d{4}");
Matcher matcher = pattern.matcher("123-456-7890");
if (matcher.find()) {
System.out.println(matcher.group()); // "123-456-7890"
}
2
3
4
5
6
7
8