正则表达式教程
正则表达式教程
正则表达式是一种用来描述文本规则的工具。它看起来像一串难读的符号,但本质上是在回答一个很朴素的问题:一段文本里,哪些内容符合我想要的模式?比如邮箱、手机号、日志时间、文件名、URL、变量名,都可以用正则表达式描述出来,然后交给编辑器、命令行工具或编程语言去查找、校验、提取和替换。
很多人第一次接触正则表达式时,会觉得它像一门“符号密语”。这种感觉很正常,因为正则把许多文字规则压缩到了很短的一行里。学习它的关键不是硬背所有符号,而是先理解一条正则表达式是怎样从左到右匹配文本的,再逐步掌握字符、数量、位置和分组这几类核心概念。下面我们从最简单的匹配开始,一点点把它拆开。
正则表达式能做什么
正则表达式最常见的用途有四类:查找、校验、提取和替换。
- 查找是指在一大段文本中找到符合规则的内容。例如在代码仓库里搜索所有以
.test.js结尾的测试文件,或者在日志里搜索所有包含ERROR的行。 - 校验是指判断一整段输入是否符合规则,例如注册时检查邮箱格式是否大致正确。
- 提取是指从文本中拿出某个部分,例如从
GET /api/users/42 HTTP/1.1中提取路径/api/users/42。 - 替换则是把符合规则的内容改成新格式,例如把日期
2026/08/07批量替换成2026-08-07。
需要注意的是,正则表达式适合处理“有明显文本模式”的问题。如果文本规则涉及复杂语义,比如判断一句话是否表达赞同,或者解析层级很深的编程语言语法,正则就不是最合适的工具。把它当成文本模式工具,而不是万能解析器,使用起来会更稳。
从普通字符开始
最简单的正则表达式就是普通字符。正则 go 会匹配文本中的连续字符 go。
文本:I love golang, and go is simple.
正则:go
匹配:I love [go]lang, and [go] is simple.这里的 g 和 o 没有特殊含义,它们只是普通字符。正则引擎会从文本左侧开始尝试匹配:先找 g,再看它后面是不是 o,如果连续满足,就认为匹配成功。
这个例子虽然简单,但它说明了正则最核心的匹配方式:正则表达式描述的是一个连续的文本片段。除非使用了特殊语法,否则每一个字符都要按顺序出现。
匹配一类字符
只匹配固定文字还不够。很多时候我们想匹配“一类字符”,例如任意数字、任意字母、任意空白。正则提供了字符类来表达这类需求。
| 写法 | 含义 | 示例 |
|---|---|---|
[abc] | 匹配 a、b、c 中任意一个字符 | [abc]at 可匹配 cat、bat |
[0-9] | 匹配任意一个数字 | [0-9] 可匹配 7 |
[a-z] | 匹配任意一个小写字母 | [a-z] 可匹配 g |
[^0-9] | 匹配任意一个非数字字符 | 可匹配 a、-、空格 |
. | 匹配任意一个字符,通常不包括换行 | a.c 可匹配 abc、a7c |
例如要匹配 cat、bat、hat 这三个单词,可以写:
[cbh]at这条正则可以拆成两部分:[cbh] 表示第一个字符必须是 c、b、h 中的一个;后面的 at 是普通字符,必须连续出现。所以它能匹配 cat,也能匹配 bat 和 hat,但不能匹配 eat。
为了让表达更简洁,正则还提供了一些常用简写。
| 写法 | 常见含义 |
|---|---|
\d | 一个数字,通常等价于 [0-9] |
\D | 一个非数字字符 |
\w | 一个单词字符,通常包括字母、数字和下划线 |
\W | 一个非单词字符 |
\s | 一个空白字符,例如空格、制表符、换行 |
\S | 一个非空白字符 |
例如 \d\d\d 可以匹配连续三个数字,如 123。不过连续写三个 \d 不够优雅,下一步我们需要学习“数量”。
控制出现次数
正则表达式中,数量词用来描述前一个字符或前一个结构应该出现几次。
| 写法 | 含义 | 示例 |
|---|---|---|
* | 出现 0 次或多次 | go* 可匹配 g、go、goo |
+ | 出现 1 次或多次 | go+ 可匹配 go、goo,不能只匹配 g |
? | 出现 0 次或 1 次 | https? 可匹配 http、https |
{3} | 正好出现 3 次 | \d{3} 匹配 3 个数字 |
{2,5} | 出现 2 到 5 次 | \d{2,5} 匹配 2 到 5 个数字 |
{2,} | 至少出现 2 次 | \d{2,} 匹配至少 2 个数字 |
例如匹配一个中国大陆手机号,可以先写成:
1[3-9]\d{9}这条正则的意思是:第一个字符是 1;第二个字符是 3 到 9 之间的数字;后面再跟 9 个数字。把它连起来就是 11 位数字,并且开头符合常见手机号段。
文本:手机号是 13812345678,座机是 01012345678。
正则:1[3-9]\d{9}
匹配:手机号是 [13812345678],座机是 01012345678。这个例子也提醒我们:正则通常是在“文本中找到符合规则的一段内容”。如果只写 1[3-9]\d{9},它可以从长文本中截取手机号;如果我们想要求整段输入必须就是一个手机号,还需要使用位置边界。
限定匹配位置
位置边界不匹配具体字符,而是匹配“位置”。最常用的是行首、行尾和单词边界。
| 写法 | 含义 |
|---|---|
^ | 匹配文本开头,或多行模式下的行首 |
$ | 匹配文本结尾,或多行模式下的行尾 |
\b | 匹配单词边界 |
如果要校验整段输入是否是手机号,可以写:
^1[3-9]\d{9}$^ 表示必须从文本开头开始匹配,$ 表示必须匹配到文本结尾。这样 13812345678 可以通过,而 手机号13812345678 不会通过,因为它开头多了文字。
单词边界 \b 常用于避免误伤。例如要在代码或文章中查找独立单词 go,可以写:
\bgo\b它能匹配 go is simple 中的 go,但不会匹配 golang 中的 go。原因是 golang 里的 go 后面仍然连接着字母,那里不是单词边界。
分组与选择
当我们希望数量词作用于一整段内容,或者希望在多个模式中选择一个,就需要使用分组。
(go)+这条正则表示 go 这个整体出现一次或多次,所以它可以匹配 go、gogo、gogogo。如果不写括号,go+ 的 + 只作用在 o 上,含义就变成了 g 后面跟一个或多个 o。
选择用竖线 | 表示“或者”。
cat|dog它可以匹配 cat 或 dog。如果只想让一部分内容参与选择,应该加括号:
(cat|dog)s?这条正则可以匹配 cat、cats、dog、dogs。其中 (cat|dog) 表示先选择 cat 或 dog,后面的 s? 表示 s 可以出现 0 次或 1 次。这样比写成 cat|dogs? 更清楚,因为后者的选择范围容易被误解。
分组还有一个常见用途:提取内容。例如从日期中提取年、月、日:
^(\d{4})-(\d{2})-(\d{2})$对 2026-08-07 来说,第一个分组 (\d{4}) 捕获 2026,第二个分组捕获 08,第三个分组捕获 07。在 JavaScript、Go、Java 等语言里,都可以通过匹配结果拿到这些分组内容。
贪婪与非贪婪
数量词默认是贪婪的,也就是在满足整体匹配的前提下,尽量多匹配字符。这一点在处理 HTML、日志和带分隔符的文本时很重要。
文本:<b>Go</b><b>Java</b>
正则:<b>.*</b>这里的 .* 会尽量多匹配,所以结果不是第一个 <b>Go</b>,而是整段 <b>Go</b><b>Java</b>。因为从第一个 <b> 到最后一个 </b> 仍然满足规则。
如果希望尽量少匹配,可以在数量词后面加 ?,把它变成非贪婪模式:
<b>.*?</b>这时它会先匹配 <b>Go</b>,再匹配 <b>Java</b>。这里的 .*? 仍然表示任意字符出现多次,只是策略从“尽量多”变成了“够用就停”。
不过在能明确分隔符的情况下,通常更推荐写得具体一些:
<b>[^<]*</b>[^<]* 表示匹配多个非 < 字符。它比 .*? 更明确,因为它直接说明了标签内容里不能出现新的标签起始符。
转义特殊字符
有些字符在正则里有特殊含义,例如 ., *, +, ?, (, ), [, ], {, }, ^, $, |, \。如果你想匹配它们本身,需要加反斜杠转义。
例如匹配文件名 README.md,不能直接写:
README.md这里的 . 会表示“任意一个字符”,所以它也可能匹配 README-md。更准确的写法是:
README\.md再比如匹配价格 ¥99.00,可以写:
¥\d+\.\d{2}其中 \d+ 表示一个或多个数字,\. 表示普通小数点,\d{2} 表示两位小数。
在编程语言中还要注意字符串本身也可能需要转义。比如 Java 字符串里的 \d 通常要写成 "\\d",因为第一个反斜杠先被 Java 字符串解析,第二个反斜杠才会传给正则引擎。Go 的原始字符串可以用反引号减少这类转义,例如 `\d+`。
常见实用例子
下面这些例子不是为了追求“最完美”,而是展示正则如何一步步表达规则。实际项目中,校验规则要结合业务需求调整。
匹配邮箱
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$这条正则可以分成三段:[A-Za-z0-9._%+-]+ 匹配邮箱用户名部分,允许字母、数字、点、下划线、百分号、加号和减号;@ 匹配邮箱中的固定分隔符;[A-Za-z0-9.-]+\.[A-Za-z]{2,} 匹配域名部分,要求有一个点,并且点后面至少有两个字母。
它能匹配 hello@example.com 和 a.b+test@sub.example.cn。但邮箱标准非常复杂,如果业务场景要求非常严谨,通常不建议自己写一个巨大的正则,而是使用成熟库或采用“前端基本校验 + 后端发送验证邮件”的方式。
提取日志里的时间和级别
假设日志格式如下:
2026-08-07 10:30:12 INFO user login success可以写:
^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(INFO|WARN|ERROR)\s+(.+)$这条正则里,第一个分组提取日期,第二个分组提取时间,第三个分组提取日志级别,第四个分组提取日志正文。\s+ 用来匹配一个或多个空白字符,这比只写一个普通空格更稳,因为日志字段之间有时可能出现多个空格或制表符。
查找 Markdown 链接
Markdown 链接通常长这样:
[VuePress](https://vuejs.press/zh/)可以用下面的正则提取链接文字和地址:
\[([^\]]+)\]\(([^)]+)\)这里的 \[ 和 \] 用来匹配真实的方括号,因为方括号在正则里原本表示字符类。([^\]]+) 表示链接文字,含义是匹配一个或多个非 ] 字符。\( 和 \) 匹配真实的小括号,([^)]+) 则提取括号里的链接地址。
这个例子适合处理简单 Markdown 链接。如果链接标题里存在嵌套括号、转义字符或更复杂的 Markdown 语法,应该使用 Markdown 解析器,而不是继续把正则写得越来越复杂。
批量替换日期格式
如果文本里有日期 2026/08/07,想替换成 2026-08-07,可以查找:
(\d{4})/(\d{2})/(\d{2})替换为:
$1-$2-$3第一个分组 $1 是年份,第二个分组 $2 是月份,第三个分组 $3 是日期。很多编辑器和语言都支持这种“捕获分组 + 替换引用”的写法,不过不同工具的引用语法可能略有差异。例如有的工具使用 \1、\2、\3。
写正则表达式的思路
写正则时,不要一上来就追求一步到位。更稳的方式是先把规则翻译成自然语言,再逐段变成正则。
例如要匹配一个简单变量名,可以先描述规则:第一个字符必须是字母或下划线,后面可以是字母、数字或下划线,整体不能为空。根据这句话,可以写出:
^[A-Za-z_][A-Za-z0-9_]*$第一段 ^[A-Za-z_] 表示从开头开始,第一个字符是字母或下划线;第二段 [A-Za-z0-9_]* 表示后面可以跟任意多个字母、数字或下划线;最后的 $ 表示必须到文本结尾。
再比如要匹配简单版本号 v1.2.3,可以先描述:以 v 开头,后面有三段数字,中间用点分隔。于是可以写:
^v\d+\.\d+\.\d+$如果版本号前面的 v 可有可无,就把 v 改成 v?:
^v?\d+\.\d+\.\d+$这种“自然语言规则 -> 分段翻译 -> 用样例验证”的过程,比直接凭感觉堆符号更可靠。
调试正则表达式
调试正则时,建议准备两类样例:应该匹配的正例,以及不应该匹配的反例。比如调试手机号时,正例可以是 13812345678,反例可以是 23812345678、1381234567、手机号13812345678。如果正例不通过,说明规则太严格;如果反例通过,说明规则太宽松。
还要尽量从小片段开始验证。先验证 1[3-9] 能不能匹配手机号开头,再加上 \d{9},最后再加 ^ 和 $。每次只增加一点点规则,错误会更容易定位。
另外,不同语言和工具的正则特性并不完全一样。JavaScript、Go、Java、Python、VS Code、grep 都支持常见语法,但在命名分组、零宽断言、多行模式、Unicode 处理等细节上可能不同。写到复杂表达式时,要以当前工具或语言的正则文档为准。
小结
正则表达式的核心并不复杂:普通字符负责匹配固定内容,字符类负责匹配一类字符,数量词负责控制出现次数,边界负责限制位置,分组负责组合和提取。真正容易出错的地方,通常不是某个符号本身,而是没有先想清楚文本规则,或者没有准备足够的正例和反例。
学习正则最好的方式,是把它当作日常文本处理工具来练习。遇到日志、文件名、链接、日期、命令输出时,先尝试用自然语言描述模式,再写出正则验证。写得多了以后,那些看起来密密麻麻的符号就会变成一套清晰的表达方式。
