正则表达式核心语法详解:从字符匹配、重复、位置到回溯引用与条件匹配(CS-Notes 技术知识库)
正则表达式用于文本内容的查找与替换,是程序员日常处理日志、校验输入、清洗数据的必备工具,也是技术面试的基础考点。本文以 CS-Notes 仓库的 notes/正则表达式.md 为骨架,系统讲解正则表达式的十大核心主题——匹配单个字符、匹配一组字符、元字符、重复匹配、位置匹配、子表达式、回溯引用、前后查找与嵌入条件,并结合仓库内 Linux 工具笔记、Leetcode 位运算题解 与剑指 Offer 数值字符串题解 中的真实用例,说明这些语法在实际开发与工程工具中的落地方式。读完本文,你将掌握一套完整、可直接套用的正则表达式语法框架,并能读懂大多数工程代码中的匹配规则。
说明:正则表达式本身不是一种语言或软件,它内置于各类编程语言与软件产品(如 grep、sed、编辑器、IDE)之中。各实现之间的语法细节存在差异,本文以通用语法为主,遇到差异会专门指出。
一、匹配单个字符:理解"元字符"与转义
. 是正则表达式中最基本的元字符,它可以匹配任何一个单个字符。注意两个关键点:
- 在绝大多数实现中,
.不能匹配换行符; .是元字符,代表的是"任意单个字符"这一特殊含义,而不是字面意义上的句点。若想匹配字符.本身,需要在它前面加上\进行转义,写作\.。
正则表达式默认区分大小写,但也有部分实现/工具提供了不区分大小写的模式开关(例如 grep 的 -i 选项,参见 notes/Linux.md 第 1020 行附近对 grep 参数的说明)。
以正则 C.C2018 为例,. 依次吞掉一个普通字符:
- 正则表达式:
C.C2018 - 匹配结果:在文本
My name is CyC2018.中,C.C2018成功匹配到CyC2018片段(C + 任意单字符 y + C2018)。
这里体现了一个贯穿全文的原则:先按普通字符匹配字面文本,再处理元字符的特殊语义,而对元字符语义不熟悉时最容易出错的就是"想匹配字面符号却忘了转义"。
二、匹配一组字符:字符集合 []、区间与取非
当需要匹配"若干个字符中的任意一个"时,使用 [ ] 定义一个字符集合:
0-9、a-z表示一个字符区间,区间范围按 ASCII 码顺序确定;字符区间只能在[ ]内部使用。-(连字符)只有在[ ]之内才是元字符,在[ ]之外它只是一个普通字符,例如a-b匹配的就是字面的 "a-b"。^出现在[ ]中时表示取非操作,即匹配"不在该集合中的任意字符"。
应用示例:匹配以 abc 为开头、且最后一个字母不为数字的字符串
- 正则表达式:
abc[^0-9] - 匹配结果:
abcd(a b c + 非数字 d,匹配)abc1(最后是数字 1,不匹配)abc2(最后是数字 2,不匹配)
需要提醒的是,[^0-9] 这类"排除型字符集合"在匹配成功时会占用一个字符位置,因此上面的表达式要求 abc 之后必须还跟着一个"非数字"的字符,纯 abc 结尾并不满足。
三、元字符分类速查:空白、数字、字母数字与进制
元字符是赋予正则表达式强大能力的基础,可以按用途分三类记忆。
匹配空白字符
| 元字符 | 说明 |
|---|---|
[\b] |
回退(删除)一个字符(Backspace) |
\f |
换页符 |
\n |
换行符 |
\r |
回车符 |
\t |
制表符 |
\v |
垂直制表符 |
行结束符随操作系统而异:\r\n 是 Windows 中的文本行结束标签,而在 Unix/Linux 中为 \n。
由此可推出一条实用技巧:\r\n\r\n 可以匹配 Windows 下的空白行——它匹配两个连续的行尾标签,而这正是两条记录之间空白行的形态。
匹配特定字符
- 数字元字符
| 元字符 | 说明 |
|---|---|
\d |
数字字符,等价于 [0-9] |
\D |
非数字字符,等价于 [^0-9] |
- 字母数字元字符
| 元字符 | 说明 |
|---|---|
\w |
大小写字母、下划线和数字,等价于 [a-zA-Z0-9_] |
\W |
对 \w 取非 |
- 空白字符元字符
| 元字符 | 说明 |
|---|---|
\s |
任何一个空白字符,等价于 [\f\n\r\t\v] |
\S |
对 \s 取非 |
按进制表示的字符
除上述助记元字符外,还可以用数值直接引用 ASCII 字符:\x 匹配十六进制字符,\0 匹配八进制字符。例如 \xA 对应值为 10 的 ASCII 字符,也就是 \n(换行符)。这类写法在需要精确表达不可见控制字符时非常有用。
补充说明:
\d、\w、\s的小写/大写形式互为"取非"关系,这是正则表达式设计中高度规律化的一组约定,记住任意一个即可通过大写/小写切换推导出它的补集。
四、重复匹配:量词、区间量词与贪婪/懒惰
单个字符或字符集合只能匹配固定的一次,要表达"出现若干次"就需要重复匹配元字符:
+:匹配 1 个或者多个字符;*:匹配 0 个或者多个字符;?:匹配 0 个或者 1 个字符。
应用:匹配邮箱地址
- 正则表达式:
[\w.]+@\w+\.\w+ - 匹配结果:从
abc.def@qq.com中匹配出abc.def@qq.com。
这里的关键细节是:[\w.] 匹配的是字母数字或者 .,在其后加上 + 表示"字母数字与点组成的整体出现一次或多次";并且在字符集合 [ ] 内部,. 不再是元字符,无需转义。邮箱主体 abc.def 因此被成功匹配,而 @ 和后面的域名部分则用 \w+、\.、\w+ 依次衔接。
区间量词
用 { } 可以更精确地指定重复次数:
{n}:匹配恰好 n 个字符;{m,n}:匹配 m~n 个字符;{m,}:至少匹配 m 个字符。
贪婪与懒惰匹配
* 和 + 都属于贪婪型元字符,会尽可能多地匹配内容。要转换为懒惰型,只需在其后加 ?,例如 *?、+? 和 {m,n}?。
示例
- 正则表达式:
a.+c - 匹配结果:在文本
abcabcabc上,由于+是贪婪型的,.+会尽可能多地匹配,因此整段abcabcabc都被匹配,而不是只匹配前面的abc。改用懒惰型a.+?c则只匹配第一个abc。
这一"贪婪 vs 懒惰"的差异是文本解析中非常常见的坑:例如希望截取两个标记之间的最短内容时,忘记加 ? 会导致匹配结果跨越多个标记,得到远超预期的长文本。
工程对照:区间量词在命令行工具中需要注意转义差异。在 notes/Linux.md 的"九、正则表达式"一节提到,基本正则表达式(BRE,如 grep 默认)中
{与}在 shell 里有特殊意义,需写成a\{2,5\}才能表示"a 出现 2~5 次";而扩展正则(ERE,如grep -E、egrep、sed -r)中直接写{2,5}即可。这说明正则语法在不同工具间的"口味"并不完全一致,使用前应确认当前工具遵循哪种标准。
五、位置匹配:单词边界与字符串边界
位置匹配不消耗字符,它只断言"当前位置是否符合条件"。
单词边界
\b匹配一个单词的边界,边界是指位于\w(单词字符)与\W(非单词字符)之间的位置;\B匹配一个不是单词边界的位置。
由于 \b 只匹配位置而不匹配字符,因此 \babc\b 匹配出来的实际内容长度为 3 个字符(abc),前后两个 \b 只是锚定"abc 前后都不是单词字符"。
字符串边界
^匹配整个字符串的开头;$匹配整个字符串的结尾。
注意 ^ 的双重身份:在字符集合 [ ] 内用作取非(见第二节),在字符集合外用作匹配字符串开头。
在**分行匹配模式(multiline)**下,换行会被当作字符串边界来处理,即 ^ 与 $ 可以分别匹配每一行的行首与行尾。
应用:匹配代码中以 // 开始的注释行
要精确提取 Java 风格代码里的整行注释(前面允许有缩进空白),可把"行首锚点、可选空白、注释符转义、行尾锚点"组合起来:
- 正则表达式:
^\s*\/\/.*$
该式的分解含义是:^ 匹配行首 → \s* 允许行首存在若干空白缩进 → \/\/ 转义匹配字面 // → .* 匹配注释内容到行尾 → $ 锚定行尾。若要对多行代码逐行应用,则需要开启工具的 multiline 模式,使 ^/$ 按行生效(否则 ^ 只锚定整个字符串的开头,只能命中第一处)。
匹配结果(对如下代码,仅第 2、5 行被匹配):
public void fun() {// 注释 1int a = 1;int b = 2;// 注释 2int c = a + b;}
工程对照:位置锚点是"删除某类行"类任务的基石。以 notes/Linux.md 中的
grep为例,grep -n结合正则即可只输出匹配行及其行号;同理,配合grep -v(反向选择)可以过滤掉所有匹配注释规则的行,实现快速剔除注释。
六、子表达式:用 () 分组与用 | 表达"或"
使用 ( ) 可以定义一个子表达式(分组)。子表达式的内容可以被当成一个独立元素看待——如同一个"超字符",可以继续对其使用 *、+、{m,n} 等量词。子表达式允许嵌套,但嵌套层次过深会使可读性急剧下降,应谨慎使用。
示例
- 正则表达式:
(ab){2,} - 匹配结果:
ababab((ab)作为整体重复 2 次以上)。
| 是"或"元字符,它把左右两边的内容分别视为完整独立的候选,只要有一边匹配即可。为避免歧义,通常配合子表达式划定候选边界:
- 正则表达式:
(19|20)\d{2} - 匹配结果:
190020101020(不以 19 或 20 开头,不匹配)
综合应用:匹配 IP 地址
IP 地址的每一段都是 0~255 的数字,直接写 \d{1,3} 无法排除 256、999 之类的非法取值。正确做法是把 0~255 拆解为若干互斥分支:
-
一位数字;
-
不以 0 开头的两位数字;
-
以 1 开头的三位数;
-
以 2 开头、第二位为 0~4 的三位数;
-
以 25 开头、第三位为 0~5 的三位数。
-
正则表达式:
((25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))\.){3}(25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))
结构上可拆解为:前半段 (…\.){3} 匹配三组"合法段 + 点",每组用子表达式与 | 穷举 0~255 的五种形态;后半段复用同一组子表达式,去掉末尾的点,从而匹配最后一段。
- 匹配结果:
192.168.0.1(匹配)00.00.00.00(段以 0 开头且多出首位,不匹配)555.555.555.555(段超 255,不匹配)
工程对照:这种"子表达式 + 或"的分支穷举思路在数据校验类题解中很典型。例如 notes/20. 表示数值的字符串.md 用 Java 的
String.matches()校验数值字符串:[+-]?\d*(\.\d+)?([eE][+-]?\d+)?。该表达式将字符集合([+-])、量词?/*/+、分组(…)?与可选小数/指数部分结合起来,是本节语法的综合应用——可见正则分组与量词的组合能力足以表达复杂的结构化约束。
七、回溯引用:引用前面捕获的内容
回溯引用使用 \n 来引用某个子表达式,其中 n 为子表达式的序号,从 1 开始。回溯引用的规则是:引用处匹配的内容必须与该子表达式此前匹配到的内容完全一致。比如某子表达式匹配到了 abc,则回溯引用处也必须匹配 abc。
应用:匹配 HTML 中合法的标题元素
检查开闭标签是否配对(<h1>…</h1> 合法,而 <h3>…</h1> 不合法):
- 正则表达式:
<(h[1-6])>\w*?<\/\1>
\1 回溯引用了子表达式 (h[1-6]) 匹配到的具体内容——若开标签匹配到的是 h2,那么 \1 处也必须是 h2。同时 \w*? 使用懒惰匹配,只吞下标题文本,不会越界吞到闭合标签。对 </ 中的 / 用 \/ 转义以避免与模式分隔符冲突。
- 匹配结果:
<h1>x</h1>(标签配对,匹配)<h2>x</h2>(标签配对,匹配)<h3>x</h1>(h3与h1不一致,不匹配)
替换:引用分组重构文本
在实际替换场景中,多数工具用 $1、$2…(而非常用 \1)来引用分组捕获结果。替换通常需要查找与替换两条规则配合使用。
应用:修改电话号码格式
- 原始文本:
313-555-1234 - 查找正则:
(\d{3})(-)(\d{3})(-)(\d{4})(共 5 个子表达式:3 位区号、-、3 位局号、-、4 位号码) - 替换正则:
($1) $3-$5(在子表达式 1 的查找结果外加(),再加一个空格,以-连接子表达式 3 与 5 的结果) - 替换结果:
(313) 555-1234
大小写转换
部分实现还提供了面向替换文本的大小写控制元字符:
| 元字符 | 说明 |
|---|---|
\l |
把下个字符转换为小写 |
\u |
把下个字符转换为大写 |
\L |
把 \L 和 \E 之间的字符全部转换为小写 |
\U |
把 \U 和 \E 之间的字符全部转换为大写 |
\E |
结束 \L 或者 \U |
应用:把文本的第二个和第三个字符转换为大写
- 原始文本:
abcd - 查找正则:
(\w)(\w{2})(\w)(四字符被拆成 1 + 2 + 1 三组) - 替换:
$1\U$2\E$3(保留第 1 组,第 2 组整体大写后由\E结束大写状态,第 3 组保持原样) - 结果:
aBCd
工程对照:把子表达式当作"可复用的捕获单元"这一思想在算法题解中同样常见。仓库的 notes/Leetcode 题解 - 位运算.md 提供了一个精简示例:判断一个数是否为 4 的幂可以先把数转成 4 进制字符串,再用正则
10*校验"以 1 开头、后跟任意个 0"的结构。反过来,对捕获与回溯的充分理解是阅读这类题解与编写字符串解析代码的前提。
八、前后查找(环视):只断言、不消费
前后查找规定了匹配内容的首/尾应该是什么,但不把首尾这些"辅助定位"的内容包含进最终匹配结果中——它们只做断言。
- 向前查找(Lookahead) 用
(?=)定义:它规定匹配内容"之后(尾部)"必须出现的内容,该内容写在?=之后。命名上"向前"即向右侧的后续文本方向查看。 - 向后查找(Lookbehind) 用
(?<=)定义:它规定匹配内容"之前(首部)"必须出现的内容。 - 语言支持差异需注意:JavaScript 不支持向后查找;Java 对它的支持也不完善。因此跨平台使用前务必核对目标运行环境。
应用:找出邮箱地址 @ 字符前面的部分
- 正则表达式:
\w+(?=@) - 匹配结果:在
abc@qq.com中仅匹配出abc,@只用于断言定位、不出现在结果里。
对前后查找取非:把 = 替换为 ! 即可,例如 (?=) 变体为 (?!)、(?<=) 变体为 (?<!)。取非操作使匹配"首尾不符合要求"的内容——即负向前查找/负向后查找。
前后查找的价值在于"只看不取":既要验证上下文格式(如前面必须是某关键字、后面必须是分隔符),又不希望这些上下文字符被整体替换或提取,非常适合做分词、脱敏、替换等场景。
九、嵌入条件:条件性匹配
嵌入条件让表达式"有条件"地决定是否继续匹配后续内容,可分为两类。
回溯引用条件
条件为"某个子表达式是否发生过匹配":若条件成立,则必须继续匹配条件表达式后面的内容。
- 正则表达式:
(\()?abc(?(1)\))
解析:子表达式 (\()(转义左括号)整体由 ? 修饰为可选——即括号可匹配 0 或 1 次;?(1) 是条件判断,当子表达式 1 匹配成功(出现过左括号)时条件成立,则必须执行后面的 \) 去匹配右括号;若子表达式 1 未匹配,则条件不成立,无需(也不允许)出现右括号。
- 匹配结果:
(abc)(左右括号成对,匹配)abc(无括号,匹配)(abc(只有左括号没有右括号,不匹配)
前后查找条件
条件为"定义的首/尾是否匹配":若匹配,则继续执行后面的匹配;注意这里的首/尾内容不包含在最终匹配结果中。
- 正则表达式:
\d{5}(?(?=-)-\d{4})
解析:(?=-) 是前向查找条件,只有当 5 位数字之后确实以 - 作为前向查找的结尾、即条件成立时,才继续匹配 -\d{4}(连字符加 4 位数字);若后跟的不是 -,则 5 位数字本身就是完整匹配。
- 匹配结果:
11111(后面无-,5 位数字即匹配)22222-(有-但缺 4 位数字,不匹配)33333-4444(5 位 + 条件成立后完整衔接-4444,匹配)
嵌入条件的实际价值在于表达"上下文决定结构"的文本:例如带括号的数字、带区号的电话、可选单位后缀等——当某部分出现时其配套部分必须出现,二者需成对存在。
十、在仓库中的进一步实践建议
正则语法只有落到真实工具里才算掌握,仓库内有多处可直接练习与验证的上下文:
- 命令行正则(grep):参见 notes/Linux.md 的"九、正则表达式"章节。
grep的名字即 g/re/p(globally search a regular expression and print),常用参数包括-c(统计匹配行数)、-i(忽略大小写)、-n(输出行号)、-v(反向选择)与--color=auto(高亮命中)。尤其要记得:默认基本正则语法下{、}需要写成\{、\}(如grep -n 'a\{2,5\}' file),这与文本编辑器/高级语言中习惯的写法不同。 - Java 语言中的正则校验:notes/20. 表示数值的字符串.md 给出了用
String.matches()验证数值字符串的完整解法;notes/Leetcode 题解 - 位运算.md 则用Integer.toString(num, 4).matches("10*")判断 4 的幂。注意 Java 的matches()要求整个字符串完全匹配,与"查找包含子串"的语义不同,必要时以.*包裹或改用find()。 - 动态规划实现正则引擎(面试高频):notes/19. 正则表达式匹配.md 要求用 DP 实现仅含
.(任意单字符)与*(前面字符出现任意次,含 0 次)的正则匹配。该题是理解正则引擎"状态与回溯"本质的最佳入门,建议配合本文的量词、贪婪小节一起复习。
参考资料
- Ben Forta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.
关联阅读:本文的完整目录入口位于仓库根目录 README.md 的"工具"分类,同一分类下还有 Git.md、Docker.md、构建工具.md 等实践类笔记,可与正则表达式结合形成完整的开发者工具链知识体系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00