正则表达式从入门到深入:模式、断言与优化
正则表达式完全指南:从入门到深入的模式、断言与优化
引言:为什么每个开发者都需要掌握正则表达式
正则表达式(Regular Expression,简称regex)是一种用于匹配字符串中字符组合的模式。它几乎存在于所有编程语言和文本编辑器中,是处理验证、搜索、替换、数据清洗等任务的终极武器。无论你是前端、后端还是数据分析师,掌握正则都能将你的文本处理效率提升一个量级。本教程从零开始,系统讲解正则的语法、核心机制与优化技巧,帮助你从“能写”进化到“会写”。
基础语法:元字符与字面量
正则表达式由字面量字符(比如 a、1)和元字符(有特殊含义的符号)组成。理解元字符是第一步。
常用元字符速查表
| 元字符 | 含义 | 示例 | 匹配结果 |
|---|---|---|---|
. |
匹配除换行符外的任意单个字符 | c.t |
cat, c2t, c t |
\w |
匹配字母、数字、下划线 (等同于 [a-zA-Z0-9_]) |
\w+ |
hello_123 |
\W |
匹配非单词字符(与 \w 相反) |
\W |
@, #, 空格 |
\d |
匹配一个数字字符 ([0-9]) |
\d{3} |
123, 007 |
\D |
匹配一个非数字字符 | \D |
a, -, ! |
\s |
匹配空白符(空格、制表符、换行等) | a\sb |
a b, a\tb |
\S |
匹配非空白字符 | \S+ |
hello |
\n |
换行符;\t 制表符;\r 回车 |
- | - |
^ |
匹配字符串的开始(在多行模式下也匹配行首) | ^Hello |
以 Hello 开头的行 |
$ |
匹配字符串的结束(多行模式下也匹配行尾) | end$ |
以 end 结尾的行 |
| ` | ` | 逻辑"或",匹配左边或右边的表达式 | `cat |
() |
分组,并捕获匹配的内容 | (ha)+ |
ha, haha, hahaha |
(?:) |
非捕获分组,仅用于分组不保存引用 | (?:ab)+ |
abab |
[] |
字符类,匹配括号内的任意一个字符 | [aeiou] |
任意元音字母 |
[^] |
否定字符类,匹配不在括号内的任意字符 | [^0-9] |
任意非数字 |
转义字符
如果你想匹配元字符本身(如 .、*、?),需要在前面加反斜杠 \。
- 匹配一个点号:
\. - 匹配星号:
\* - 匹配问号:
\?
量词:控制匹配次数
量词用于指定前面的子表达式出现的次数。
| 量词 | 含义 | 示例 | 可能匹配 |
|---|---|---|---|
* |
重复0次或更多次(贪婪) | ab*c |
ac, abc, abbc |
+ |
重复1次或更多次(贪婪) | ab+c |
abc, abbbc |
? |
重复0次或1次(可选) | colou?r |
color, colour |
{n} |
恰好重复 n 次 | \d{4} |
2023, 0000 |
{n,} |
至少重复 n 次 | a{2,} |
aa, aaaa |
{n,m} |
重复 n 到 m 次 | \d{2,4} |
12, 123, 1234 |
贪婪与懒惰匹配
默认情况下,量词是贪婪的,尽可能多地匹配字符。在量词后面加上 ? 可以转为懒惰(非贪婪)匹配,即尽可能少地匹配。
字符串: <div>hello</div>
贪婪模式: <.*> → 匹配整个 <div>hello</div>
懒惰模式: <.*?> → 匹配 <div>
记忆技巧:贪婪是“一口吞到底”,懒惰是“浅尝辄止”。通常解析 HTML 或引号内的内容时,懒惰模式非常有用。
字符类:更灵活的匹配集合
字符类 [] 可以定义一个字符集合,匹配其中任意一个字符。
[abc]匹配 'a'、'b' 或 'c'。[a-z]匹配任意小写字母,[A-Z]大写字母,[0-9]数字。- 组合:
[a-zA-Z0-9_]等价于\w。 - 在字符类内部,大多数元字符(除了
^,-,\,])都失去特殊意义,被视为普通字符。例如[.]只匹配点号本身。
否定字符类
[^...] 匹配不在集合内的任意字符。比如 [^0-9] 匹配非数字。
断言:零宽匹配的艺术(前瞻与后顾)
断言(Assertions)也叫零宽断言,它们不消耗字符,只判断某个位置是否满足条件。这是正则从入门到进阶的关键。
前瞻断言(Lookahead)
- 正向前瞻
(?=pattern):匹配一个位置,该位置后面能匹配 pattern。 - 负向前瞻
(?!pattern):匹配一个位置,该位置后面不能匹配 pattern。
示例:匹配后面跟着 "ing" 的 "do"
字符串: doing, doer, do
正则: do(?=ing) // 匹配 doing 中的 do,但不匹配 doer 或 do
匹配后面不是数字的单词边界:
正则: \b\w+\b(?!\d) // 整个单词后面不能紧跟数字
后顾断言(Lookbehind)
- 正向后顾
(?<=pattern):匹配一个位置,该位置前面能匹配 pattern。 - 负向后顾
(?<!pattern):匹配一个位置,该位置前面不能匹配 pattern。
示例:匹配前面是 $ 的数字
字符串: $100, €50
正则: (?<=\$)\d+ // 匹配 $100 中的 100,不匹配 €50
匹配不是以 "un" 开头的单词:
正则: \b(?<!un)\w+\b
注意:后顾断言在很多语言中要求固定宽度,但 JavaScript 从 ES2018 开始支持后顾,且允许非固定宽度(但某些引擎有限制)。使用时请确认目标环境。
词边界断言
\b:匹配一个单词边界(字母数字与下划线之间、字符与非单词字符之间等位置)。\B:匹配非单词边界。
正则: \bcat\b // 只匹配独立的 "cat",不匹配 "category" 中的 cat
分组与捕获:提取与引用
捕获分组 (...)
括号不仅用于优先级控制,还会捕获匹配的内容并分配组号。组号从左到右按左括号出现顺序编号(从1开始),第0组是整个匹配。
反向引用:可以在正则内部通过 \1、\2 引用前面捕获的组。
- 示例:匹配重复单词:
\b(\w+)\s+\1\b - 解释:
(\w+)捕获一个单词,\s+至少一个空白,\1引用与第1组相同的文本。
大多数编程语言也支持在替换操作中使用 $1、$2 等引用捕获组。
非捕获分组 (?:...)
它只用于分组,不创建捕获,可提高性能并保持组号简洁。当你不需要反向引用时,请优先使用非捕获分组。
命名捕获分组
一些现代引擎支持给组命名,使代码更可读。
- JavaScript:
(?<name>pattern),通过groups.name访问。 - Python:
(?P<name>pattern)。
示例(PCRE/JS):
正则: (?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
常用标志(修饰符)
标志改变正则的匹配行为,通常在正则字面量后添加。
| 标志 | 含义 | 示例 |
|---|---|---|
i |
忽略大小写 | /hello/i |
g |
全局匹配(找到所有匹配,不只在第一个) | /a/g |
m |
多行模式,使 ^ 和 $ 匹配每行的开始/结束 |
/^test/m |
s |
单行模式(使 . 能匹配换行符) |
/.*/s |
u |
Unicode 模式(正确解析 Unicode 字符) | /\p{L}/u |
y |
粘性模式(仅从目标字符串的当前位置匹配) | /a/y |
不同语言中标志的写法不同,如 Python 中
re.IGNORECASE,JavaScript 中/pattern/gi。
实战应用模式
1. 电子邮件验证(简化版)
/^[\w.-]+@[\w.-]+\.\w{2,}$/i
解释:允许字母数字、点、下划线、连字符的用户名,@ 后是域名,最后至少2位顶级域。实际生产环境需更严谨的 RFC 模式,此可作为初步校验。
2. 密码强度校验
至少8位,包含大小写字母、数字和特殊字符:
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+]).{8,}$/
巧妙利用前瞻断言:每个 (?=.*xxx) 确保在整个字符串中存在某个字符,但不消耗字符,最后用 .{8,} 匹配整体长度。
3. 提取 URL 中的域名
https?://([^/]+)
匹配 http 或 https 协议,然后捕获到下一个斜杠前的部分,即域名。
4. 去除字符串首尾空白
/^\s+|\s+$/g
两端匹配空白并全局替换为空。
5. 将驼峰命名转为短横线命名
替换正则: /([A-Z])/g
替换内容: -$1,然后整个字符串转小写,并去除可能开头的短横线。
JavaScript 示例: 'camelCase'.replace(/[A-Z]/g, '-$&').toLowerCase().replace(/^-/, '')
$& 表示整个匹配文本。
6. 匹配 HTML 标签内容(简单场景)
使用非贪婪模式:
/<title>(.*?)<\/title>/i
注意:对于复杂 HTML,推荐使用 XML/HTML 解析器,正则不适合解析嵌套结构。
正则优化技巧:让模式跑得更快
错误的模式可能导致灾难性回溯(Catastrophic Backtracking),严重降低性能。
1. 避免模糊嵌套量词
类似 (a+)+ 或 (a*)* 的模式会导致回溯次数指数级增长。永远不要嵌套两个可变长度的量词。
错误示例:(.*)*
当字符串末尾匹配失败时,引擎将尝试无限种分组可能。
正确做法:明确使用 (.*) 或 (?:a+)* 如果确实需要,但务必确保有明确边界。
2. 使用字符类代替点号
. 在默认模式下匹配除换行符外的所有字符,范围太大容易导致过度匹配。如果知道内容的具体字符范围,用字符类 [a-zA-Z0-9] 等更高效。
3. 优先使用非捕获分组
如果不需要反向引用,使用 (?:...) 代替 (...),避免不必要的捕获存储开销。
4. 善用锚点
使用 ^ 和 $ 快速定位,减少引擎搜索范围。尤其在验证整体字符串时,一定要写锚点。
5. 使用原子组或占有量词(如果支持)
部分正则引擎(如 PCRE)支持原子组 (?>...),它一旦匹配就不会回溯交出内容。可以有效阻止回溯。
例:(?>a*)a 永远无法匹配,因为 a* 夺走所有 a 后没有剩余的 a。
6. 预编译正则对象
在循环中重复使用同一个正则表达式时,请先编译它(如 JS 中的 new RegExp() 或 Python 中的 re.compile()),避免重复解析模式。
常见错误与调试建议
- 未转义点号:
http://example.com中的点号忘记转义,http://后的点号会匹配任何字符。 - 忘记开始/结束锚点:验证时如果不加
^$,可能只匹配子串,导致误判。 - 贪婪匹配吞掉末尾:匹配引号内容时用
".*"可能从第一个引号匹配到最后一个。改用"[^"]*"或".*?"。 - 忽略换行符:默认
.不匹配换行,如果需要跨行匹配,记得使用[\s\S]或单行模式标志。
在线调试工具:推荐使用 regex101.com,它能可视化匹配步骤、解释每个 token,并显示回溯次数,是学习和排错的利器。
高级主题速览
- 条件子模式:
(?(条件)true-pattern|false-pattern),部分引擎支持。 - 递归匹配:
(?R)或\g<name>,用于匹配嵌套括号等结构(PCRE)。 - Unicode 属性:
\p{Script=Latin},\p{Letter}等,需开启u标志。 - 子程序调用:将分组当作函数调用,避免重复书写(Perl、PCRE)。
- 注释:在
(?#comment)或开启x标志(忽略空白和注释)下提升可读性。
总结:学习路径与持续提升
- 熟记元字符和量词,能随手写出常用模式。
- 动手写:从邮箱、URL、日期等常见需求练习。
- 掌握零宽断言,它们是字符串精准定位的灵魂。
- 理解引擎回溯,阅读灾难性回溯案例,培养写出高效模式的习惯。
- 善用社区和工具:参考 Regular-Expressions.info 或各语言官方文档。
正则表达式是一门“写时难,读时更难”的语言,但一旦内化,它将打开文本处理的任意门。现在就开始用本篇指南创建你的第一个正则表达式吧!