learn-regex 正则表达式入门指南:从基础匹配到前后查找与贪婪惰性匹配
learn-regex 正则表达式入门指南:从基础匹配到前后查找与贪婪惰性匹配
正则表达式(Regular Expression,简称 regex / regexp)是一组用于在文本中查找特定模式的字符或符号,广泛应用于文本替换、表单校验、按模式从字符串中提取子串等场景。本文以 learn-regex 开源教程(仓库 translations/README-tr.md 为土耳其语母版,英文原版见 README.md)为核心,系统讲解从最基础的字符匹配到元字符、字符集、重复、捕获组、锚点、前后查找、标志(Flags)以及贪婪与惰性匹配的完整知识体系,并配以可复制运行的真实示例。读完本文,你将能够独立读懂常见正则表达式、写出用户名校验等实用模式,并掌握在编程语言与命令行工具中使用正则的基本方法。
首先以一个最贴近日常开发的例子直观感受正则表达式:编写应用时,我们往往需要为「用户名」设定规则——允许字母、数字、下划线和连字符,并限制总长度,避免输入"太丑"。以下正则即可完成该校验:
![用户名校验正则示例:^[a-z0-9_-]{3,15}$,图中标注了行首锚点、允许字符集与长度限制等含义](img/regexp-tr.png)
这条正则 ^[a-z0-9_-]{3,15}$ 可以接受 john_doe、jo-hn_doe、john12_as 这样的输入;而 Jo 无法匹配,因为它包含大写字母(字符集只允许小写字母),且长度不足 3 个字符。下面我们把这幅图拆解成每一个组成部分,逐一掌握。
1. 基础匹配器(Basic Matchers)
正则表达式本质上就是一个用于在文本中搜索的字符模式。例如正则 the 表示:字母 t,后面跟着 h,再后面跟着 e,引擎会从左到右把正则中的每个字符与输入文本中的每个字符逐一比较。
"the" => The fat cat sat on <strong>the</strong> mat.
正则 123 匹配字符串 123。需要记住:正则默认是大小写敏感的,因此 The 不会匹配 the:
"The" => <strong>The</strong> fat cat sat on the mat.
也就是说,The 只会在文本中恰好出现大写 T 时命中。
2. 元字符(Meta Characters)
元字符是正则表达式的构建基石。它们并不代表自身,而是以特殊方式被解释。部分元字符具有特殊含义,并写在方括号内使用。完整清单如下:
| 元字符 | 说明 |
|---|---|
. |
匹配除换行符外的任意单个字符 |
[ ] |
字符类,匹配方括号内的任意一个字符 |
[^ ] |
取反字符类,匹配不在方括号内的任意字符 |
* |
匹配前面符号的 0 次或更多次重复 |
+ |
匹配前面符号的 1 次或更多次重复 |
? |
使前面的符号成为可选(0 或 1 次) |
{n,m} |
花括号(量词),匹配前面符号至少 n 次、至多 m 次 |
(xyz) |
字符组,按该精确顺序匹配 xyz |
| |
交替,匹配该符号之前或之后的表达式 |
\ |
转义下一个字符,允许匹配保留字符 [ ] ( ) { } . * + ? ^ $ \ | |
^ |
匹配输入的开始位置 |
$ |
匹配输入的结束位置 |
下面逐一深入每个元字符的用法。
2.1 点号(The Full Stop)
. 是最简单的元字符:它匹配任意单个字符,但不匹配回车或换行。例如 .ar 表示:任意一个字符,后面跟着 a,再后面跟着 r:
".ar" => The <strong>car</strong> <strong>par</strong>ked in the <strong>gar</strong>age.
可以看到 car、par、gar 都被命中,因为它们都由"任意字符 + ar"构成。
2.2 字符集(Character Sets)
字符集也叫字符类(Character Class),用方括号 [ ] 指定。可以在字符集内使用连字符 - 表示字符范围,且方括号内范围书写的顺序无关紧要。例如 [Tt]he 表示:大写 T 或小写 t,后面跟着 h、e:
"[Tt]he" => <strong>The</strong> car parked in <strong>the</strong> garage.
一个容易忽略的细节:字符集内部的点号是字面意义上的点。正则 ar[.] 表示:小写 a 后跟 r,再跟一个句点 . 字符——这里的 . 不再具备"任意字符"的含义:
"ar[.]" => A garage is a good place to park a c<strong>ar.</strong>
2.2.1 取反字符集(Negated Character Sets)
一般情况下 ^ 表示字符串开始,但当它紧跟左方括号书写时,表示对字符集取反。例如 [^c]ar 表示:除 c 之外的任意字符,后面跟着 a、r:
"[^c]ar" => The car <strong>par</strong>ked in the <strong>gar</strong>age.
这里 car 中的 c 被排除,因此不匹配,而 par、gar 匹配。
2.3 重复(Repetitions)
元字符 +、*、? 用来指定子模式可以出现多少次,它们在不同情境下表现不同。
2.3.1 星号(The Star)
* 匹配前面匹配器的 0 次或更多次重复。正则 a* 表示小写 a 的 0 次或多次重复;但如果它出现在字符集或字符类之后,则作用于整个字符集的重复。例如 [a-z]* 表示一行中任意数量的小写字母:
"[a-z]*" => T<strong>he</strong> <strong>car</strong> <strong>parked</strong> <strong>in</strong> <strong>the</strong> <strong>garage</strong> #21.
* 与 . 组合成 .* 可匹配任意字符串;与空白字符 \s 组合可匹配一串空白。例如 \s*cat\s* 表示:0 个或多个空格,后跟 c、a、t,再跟 0 个或多个空格:
"\s*cat\s*" => The fat<strong> cat </strong>sat on the con<strong>cat</strong>enation.
注意这里两个命中:一个是带前后空格的 cat(\s* 匹配了前后空格),一个是单词内部的 cat。
2.3.2 加号(The Plus)
+ 匹配前面字符的 1 次或更多次重复。例如 c.+t 表示:小写 c,后跟至少一个任意字符,再跟小写 t。需要特别说明:这里的 t 是句子中的最后一个 t:
"c.+t" => The fat <strong>cat sat on the mat</strong>.
因为 .+ 是贪婪匹配,它会一路延伸到句子末尾的 t 上。
2.3.3 问号(The Question Mark)
? 使前面的字符变为可选:匹配前面字符的 0 次或 1 次。例如 [T]?he 表示:可选的大写 T,后跟 h、e。对比下面两条结果可以清楚看到 ? 的作用:
"[T]he" => <strong>The</strong> car is parked in the garage.
"[T]?he" => <strong>The</strong> car is parked in t<strong>he</strong> garage.
不加 ? 时 [T]he 只能匹配 The;加了 ? 后,The 和单词中的 he 都能命中。
2.4 花括号(Braces)
花括号(又称量词 Quantifiers)用来指定某个字符或字符组可以重复的次数。例如 [0-9]{2,3} 表示:匹配 0 到 9 范围内的数字,至少 2 位、至多 3 位:
"[0-9]{2,3}" => The number was 9.<strong>999</strong>7 but we rounded it off to <strong>10</strong>.0.
第二项可以省略。[0-9]{2,} 表示匹配 2 位或更多位数字;如果连逗号也去掉,[0-9]{3} 表示恰好匹配 3 位数字:
"[0-9]{2,}" => The number was 9.<strong>9997</strong> but we rounded it off to <strong>10</strong>.0.
"[0-9]{3}" => The number was 9.<strong>999</strong>7 but we rounded it off to 10.0.
对比可知:{2,} 匹配了 4 位数字 9997,而 {3} 只匹配前 3 位 999(剩余 7 不构成完整命中,10 因不足 3 位也不匹配)。
2.5 捕获组(Capturing Groups)
捕获组是写在圆括号 (...) 中的一组子模式。之前提到:量词放在单个字符后,会重复前面的字符;而量词放在捕获组后,则重复整个捕获组。例如 (ab)* 匹配字符串 "ab" 的 0 次或更多次重复。捕获组内部还可以使用交替元字符 |。例如 (c|g|p)ar 表示:小写 c、g 或 p,后跟 a、r:
"(c|g|p)ar" => The <strong>car</strong> is <strong>par</strong>ked in the <strong>gar</strong>age.
需要特别注意的是:捕获组不仅匹配,还会捕获匹配到的字符,供宿主语言使用——宿主语言可以是 Python、JavaScript 或几乎任何在函数定义中实现了正则表达式的语言。例如在 JavaScript 中可用 match 结果的数组下标(如 $1、$2)或 Python 中 re.search(...).group(1) 取出捕获内容。
2.5.1 非捕获组(Non-Capturing Groups)
非捕获组是"匹配字符但不捕获"的分组,在圆括号内以 ? 后跟 : 表示,即 (?:...)。例如 (?:c|g|p)ar 与 (c|g|p)ar 匹配的字符完全相同,但不会创建捕获组:
"(?:c|g|p)ar" => The <strong>car</strong> is <strong>par</strong>ked in the <strong>gar</strong>age.
非捕获组在「查找并替换」功能中,或与非捕获组混用时保持输出结构清晰、避免产生多余捕获编号时非常有用。
2.6 交替(Alternation)
正则中的竖线 | 用于定义交替,它相当于多个表达式之间的"或"(OR)关系。你可能会想:字符集和交替不是一样的吗?关键区别在于——字符集工作在字符级别,而交替工作在表达式级别。例如 (T|t)he|car 表示:(大写 T 或小写 t 后跟 h、e) 或者 (小写 c 后跟 a、r) 两种情况之一成立即命中:
"(T|t)he|car" => <strong>The</strong> <strong>car</strong> is parked in <strong>the</strong> garage.
圆括号在这里用于明确分组,标明括号内任一表达式满足即可整体匹配。
2.7 转义特殊字符(Escaping Special Characters)
反斜杠 \ 在正则中用于转义下一个字符,使我们能把保留字符 { } [ ] / \ + * . $ ^ | ? 当作普通匹配字符使用。例如 . 本来用于匹配除换行外的任意字符;若要在输入中匹配字面上的句点,就必须转义它。正则 (f|c|m)at\.? 表示:小写 f、c 或 m,后跟 a、t,再跟一个可选的 . 字符:
"(f|c|m)at\.?" => The <strong>fat</strong> <strong>cat</strong> sat on the <strong>mat.</strong>
注意 fat、cat 后无句点也命中(因为 \. 可选),而 mat. 命中了包含句点的完整形式。
2.8 锚点(Anchors)
锚点用于检查匹配符号是否是输入字符串的开始符号或结束符号,共两类:脱字符 ^ 检查匹配字符是否为输入的第一个字符;美元符 $ 检查匹配字符是否为输入的最后一个字符。
2.8.1 脱字符(The Caret)
^ 用于检查匹配字符是否为输入字符串的第一个字符。对字符串 abc 应用 ^a(即 a 必须是起始字符),会命中 a;而应用 ^b 则一无所获,因为 abc 中 b 不是起始字符。再看正则 ^(T|t)he——大写 T 或小写 t 必须是字符串第一个字符,后跟 h、e。对比有无 ^ 的结果差异明显:
"(T|t)he" => <strong>The</strong> car is parked in <strong>the</strong> garage.
"^(T|t)he" => <strong>The</strong> car is parked in the garage.
加上 ^ 后,只有位于句首的 The 命中,句中的 the 不再匹配。
2.8.2 美元符号(The Dollar Sign)
$ 用于检查匹配字符是否为字符串的最后一个字符。例如 (at\.)$ 表示:小写 a 后跟 t,再跟 .,且该匹配必须位于字符串末尾:
"(at\.)" => The fat c<strong>at.</strong> s<strong>at.</strong> on the m<strong>at.</strong>
"(at\.)$" => The fat cat. sat. on the m<strong>at.</strong>
加上 $ 后,只有位于整个输入末尾的 at. 命中,中间的 cat.、sat. 不再匹配。
3. 速记字符集(Shorthand Character Sets)
正则提供了一批针对高频字符集的便捷速记:
| 速记 | 说明 |
|---|---|
. |
除换行外的任意字符 |
\w |
匹配字母数字字符:[a-zA-Z0-9_] |
\W |
匹配非字母数字字符:[^\w] |
\d |
匹配数字:[0-9] |
\D |
匹配非数字:[^\d] |
\s |
匹配空白字符:[\t\n\f\r\p{Z}] |
\S |
匹配非空白字符:[^\s] |
回顾文首的用户名校验正则 ^[a-z0-9_-]{3,15}$,其中 [a-z0-9_-] 正是字符集与范围语法的组合应用,而 \w、\d 等速记则是对此类常见范围的进一步抽象。注意速记的大写形式表示取反(如 \W 与 \w 相反),这一规律在编程中非常实用。
4. 前后查找(Lookarounds)
后顾(Lookbehind)与前瞻(Lookahead)合称前后查找(Lookaround),它们是一种特殊的非捕获组——用于匹配某个模式,但不把该模式计入匹配结果。前后查找适用于"某个模式必须被另一个模式前置或后置"的场景。
例如,要从字符串 $4.44 and $10.88 中取出所有以 $ 开头的数字,可使用正则 (?<=\$)[0-9\.]*,其含义是:获取所有包含 . 字符、且前面有 $ 字符的数字。正则中常用的前后查找符号如下:
| 符号 | 说明 |
|---|---|
?= |
正向先行断言(Positive Lookahead) |
?! |
负向先行断言(Negative Lookahead) |
?<= |
正向后行断言(Positive Lookbehind) |
?<! |
负向后行断言(Negative Lookbehind) |
4.1 正向先行断言(Positive Lookahead)
正向先行断言断言表达式的前半部分必须被断言表达式所跟随,但返回的匹配结果只包含前半部分命中的文本。定义方式是在圆括号内使用问号加等号:(?=...),断言表达式写在圆括号内等号之后。例如 (T|t)he(?=\sfat) 表示:匹配 T 或 t 后跟 h、e,且后面必须紧跟(空格 + fat):
"(T|t)he(?=\sfat)" => <strong>The</strong> fat cat sat on the mat.
只有 The 命中——因为只有它后面跟着 fat;句末的 the 后面没有跟 fat,因此不匹配。
4.2 负向先行断言(Negative Lookahead)
负向先行断言用于获取"不被某个模式跟随"的所有匹配。写法与正向先行断言相同,区别仅在于把 = 换成 !:(?!...)。例如 (T|t)he(?!\sfat) 表示:获取所有后面没有跟空格 + fat 的 The 或 the:
"(T|t)he(?!\sfat)" => The fat cat sat on <strong>the</strong> mat.
这一次反过来:The 因后跟 fat 而不匹配,句末的 the 命中。
4.3 正向后行断言(Positive Lookbehind)
正向后行断言用于获取所有"被某个特定模式前置"的匹配,写法为 (?<=...)。例如 (?<=(T|t)he\s)(fat|mat) 表示:获取所有出现在 The 或 the(含空格)之后的 fat 或 mat:
"(?<=(T|t)he\s)(fat|mat)" => The <strong>fat</strong> cat sat on the <strong>mat</strong>.
The fat 中的 fat 和 the mat 中的 mat 命中,cat 与 sat 因不在断言之后而不在匹配结果中。
4.4 负向后行断言(Negative Lookbehind)
负向后行断言用于获取所有"不被某个特定模式前置"的匹配,写法为 (?<!...)。例如 (?<!(T|t)he\s)(cat) 表示:获取所有没有出现在 The 或 the 之后的 cat:
"(?<!(T|t)he\s)(cat)" => The cat sat on <strong>cat</strong>.
开头的 cat(跟在 The 之后)不匹配,句末的 cat 命中。
5. 标志(Flags)
标志(Flags)也叫修饰符(Modifiers),因为它们会修改正则表达式的输出行为。标志可以按任意顺序、任意组合使用,是 RegExp 的有机组成部分:
| 标志 | 说明 |
|---|---|
i |
大小写不敏感:匹配对大小写不敏感 |
g |
全局搜索:匹配所有实例,而不仅是第一个 |
m |
多行:锚点元字符在每一行上生效 |
5.1 大小写不敏感(Case Insensitive)
i 修饰符用于执行大小写不敏感的匹配。例如 /The/gi 表示:大写 T 后跟 h、e,结尾的 i 让引擎忽略大小写;同时配合了 g 标志,因为我们想在整个输入串中搜索该模式。对比效果:
"The" => <strong>The</strong> fat cat sat on the mat.
"/The/gi" => <strong>The</strong> fat cat sat on <strong>the</strong> mat.
加上 gi 后,句中的小写 the 也被命中。
5.2 全局搜索(Global Search)
g 修饰符用于全局匹配(找出所有匹配,而不是在第一个匹配后就停止)。例如 /.(at)/g 表示:除换行外的任意字符后跟 a、t。因为末尾提供了 g,引擎会找出输入中所有匹配,而非默认只找第一个:
"/.(at)/" => The <strong>fat</strong> cat sat on the mat.
"/.(at)/g" => The <strong>fat</strong> <strong>cat</strong> <strong>sat</strong> on the <strong>mat</strong>.
无 g 时只命中第一个 fat;加 g 后 fat、cat、sat、mat 全部命中。
5.3 多行(Multiline)
m 修饰符用于执行多行匹配。前面讲过锚点 (^, $) 用于检查模式是否在输入的开始或结束位置;而如果希望锚点在每一行都生效,就需要 m 标志。例如 /at(.)?$/gm 表示:小写 a 后跟 t,之后可选地跟随除换行外的任意字符;由于 m 标志,引擎会匹配输入中每一行末尾的模式:
"/.at(.)?$/" => The fat
cat sat
on the <strong>mat.</strong>
"/.at(.)?$/gm" => The <strong>fat</strong>
cat <strong>sat</strong>
on the <strong>mat.</strong>
不加 m 时只匹配整个输入的末尾(mat.);加 gm 后,每行末尾的 fat、sat、mat. 都命中。
6. 贪婪与惰性匹配(Greedy vs Lazy Matching)
默认情况下,正则执行贪婪匹配——匹配结果尽可能长。可以通过在量词后加 ? 实现惰性匹配——匹配结果尽可能短。经典的对比示例:
"/(.*at)/" => <strong>The fat cat sat on the mat</strong>.
.* 贪婪地延伸到最后一个 at,整句话都被匹配。
"/(.*?at)/" => <strong>The fat</strong> cat sat on the mat.
加上 ? 后变成惰性匹配,.*? 在找到第一个满足条件的 at 后就停止,结果缩短为 The fat。
这一机制在解析 HTML、日志文本等场景中尤为关键:贪婪匹配往往误伤过多内容,而惰性匹配(如 .*?)能精确收敛到目标片段。
小结与继续深入
至此,我们已走完 learn-regex 教程的完整主线:从基础匹配器到元字符体系(点号、字符集、取反字符集、重复、花括号量词、捕获组与非捕获组、交替、转义、锚点),再到速记字符集、前后查找四大断言、三大标志(i/g/m)以及贪婪与惰性匹配。用本文开头的用户名校验正则 ^[a-z0-9_-]{3,15}$ 做个复盘,正好串联起本教程的核心知识:
^与$:锚点,要求从行首到行尾整串匹配;[a-z0-9_-]:字符集 + 范围语法,限定允许的字符(小写字母、数字、下划线、连字符);{3,15}:花括号量词,限定长度在 3 到 15 个字符之间。
本仓库是 MIT 协议下开源的教程型项目(LICENSE.md),英文主文档为 README.md,本文所依据的土耳其语版位于 translations/README-tr.md,其余多语言翻译(德、西、法、中、日、韩、俄、越南、希伯来、匈牙利、波兰等)也收录在 translations 目录中,可对照学习。文中所有示例均可直接在支持正则的调试工具或您所使用的编程语言环境中运行验证;建议在动手实践时逐步叠加 g、i、m 等标志,观察匹配结果的差异,这是快速内化正则语法的有效方式。