正则表达式完全指北
什么是正则表达式
正则表达式(Regular Expression,简称 regex 或 regexp)是一种描述字符串模式的微型语言。它用一个简洁的字符串来「匹配、查找、替换」符合某种规则的文本,广泛应用于表单校验、日志分析、数据清洗、爬虫提取等场景。
几乎所有主流语言都内置了正则引擎(JavaScript、Python、Java、Go、Rust、PHP、Ruby……),核心语法高度一致,只是 API 和少数特性略有差异。本文以通用语法为主线讲解,并在末尾给出不同语言的用法对比。
阅读提示:本文所有示例中,
//之间是正则模式,=>后面是匹配结果,✅表示命中,❌表示不命中。
基础语法
字面量字符
最简单的正则就是「按字面匹配」。写什么字符,就匹配什么字符:
模式:cat
"the cat sat" => 匹配 "cat"
"category" => 匹配 "cat"(category 开头的 cat)
正则默认区分大小写,cat 不会匹配 Cat。若要忽略大小写,使用 i 标志(见后文)。
元字符
以下字符在正则中具有特殊含义,称为元字符:
| 元字符 | 含义 |
|---|---|
. | 匹配任意单个字符(除换行符外,除非用 s 标志) |
^ | 匹配字符串开头 |
$ | 匹配字符串结尾 |
* | 重复 0 次或多次 |
+ | 重复 1 次或多次 |
? | 重复 0 次或 1 次(可选) |
{n} {n,} {n,m} | 重复指定次数 |
[...] | 字符类,匹配其中任意一个字符 |
(...) | 分组与捕获 |
| ` | ` |
\ | 转义,或引入预定义字符 |
\d \w \s | 预定义字符类(见下文) |
要匹配元字符本身,用 \ 转义。例如匹配字面的 .、*、?:
模式:a\.b
"a.b" => ✅ 匹配 "a.b"
"axb" => ❌(. 被转义后只能匹配字面点号)
模式:\$\d+
"$100" => ✅ 匹配 "$100"
点号 . 与通配
. 是最常用的「通配符」,匹配任意单个字符(默认不含换行符):
模式:c.t
"cat" => ✅
"c@t" => ✅
"co t" => ✅(中间是空格也算一个字符)
"ct" => ❌(. 必须匹配一个字符)
如果希望 . 也能匹配换行符,在 JavaScript 中使用 s(dotAll)标志:/a.b/s。
字符类
基本字符类 [...]
方括号表示「匹配其中任意一个字符」:
模式:[aeiou] 匹配任意一个元音字母
"hello" => 匹配 "e"、"o"
模式:gr[ae]y 匹配 gray 或 grey
"gray" => ✅
"grey" => ✅
"groy" => ❌
范围
在字符类内用 - 表示范围:
模式:[a-z] 匹配任意小写字母
模式:[A-Z] 匹配任意大写字母
模式:[0-9] 匹配任意数字
模式:[a-zA-Z0-9] 匹配字母或数字
常见范围速查:
| 范围 | 含义 |
|---|---|
[a-z] | 小写字母 a–z |
[A-Z] | 大写字母 A–Z |
[0-9] | 数字 0–9 |
[a-f] | 十六进制字符 a–f |
[\u4e00-\u9fa5] | 基本中文字符 |
范围依据字符的 Unicode 码点。
[A-z]看似涵盖所有字母,但实际包含了[ \ ] ^ _ `等字符,不推荐使用。
取反字符类 [^...]
在方括号开头加 ^ 表示「匹配不在列表中的任意字符」:
模式:[^0-9] 匹配任意非数字字符
"abc123" => 匹配 "a"、"b"、"c"
模式:[^aeiou] 匹配任意非元音字符
注意:^ 只有在字符类开头才表示取反,写在中间就是普通字符:[a^b] 匹配 a、^、b。
预定义字符类
正则内置了一批简写,等价于常用字符类,使用频率极高:
| 简写 | 等价写法 | 含义 |
|---|---|---|
\d | [0-9] | 数字 |
\D | [^0-9] | 非数字 |
\w | [A-Za-z0-9_] | 单词字符(字母、数字、下划线) |
\W | [^A-Za-z0-9_] | 非单词字符 |
\s | [ \t\n\r\f\v] | 空白字符(空格、制表符、换行等) |
\S | [^ \t\n\r\f\v] | 非空白字符 |
. | — | 任意字符(除换行) |
示例:
模式:\d\d\d-\d\d\d\d
"123-4567" => ✅
模式:\w+@\w+
"user@host" => ✅
模式:\S+ 匹配连续非空白字符(常用于提取单词)
"hello world" => 匹配 "hello"、"world"
其他特殊转义
| 写法 | 含义 |
|---|---|
\t | 制表符 Tab |
\n | 换行符 |
\r | 回车符 |
\f | 换页符 |
\v | 垂直制表符 |
\0 | NUL 字符 |
\xHH | 十六进制字符(如 \x41 = A) |
\uHHHH | Unicode 字符(如 \u4e2d = 中) |
量词
量词控制前一个元素重复的次数。
| 量词 | 含义 | 等价 |
|---|---|---|
* | 0 次或多次 | {0,} |
+ | 1 次或多次 | {1,} |
? | 0 次或 1 次(可选) | {0,1} |
{n} | 恰好 n 次 | — |
{n,} | 至少 n 次 | — |
{n,m} | n 到 m 次 | — |
示例:
模式:colou?r u 可有可无
"color" => ✅
"colour" => ✅
模式:\d{3} 恰好 3 位数字
"abc123" => 匹配 "123"
模式:\d{4,} 至少 4 位数字
"1234567" => ✅
模式:\d{2,4} 2 到 4 位数字
"12" => ✅
"1234" => ✅
"12345" => 匹配前 4 位 "1234"(默认贪心但受上限制约)
贪婪与非贪婪
量词默认是贪婪(greedy)的,会尽可能多地匹配。在量词后加 ? 变为非贪婪(lazy / 懒惰),尽可能少地匹配:
模式:<.*> 贪婪
"<a><b>" => 匹配整个 "<a><b>"(一直吃到最后的 >)
模式:<.*?> 非贪婪
"<a><b>" => 只匹配 "<a>"(遇到第一个 > 就停)
三种匹配模式对比(以 a+ 为例作用于 "aaa"):
| 模式 | 写法 | 结果 | 说明 |
|---|---|---|---|
| 贪婪 | a+ | aaa | 尽量多匹配 |
| 非贪婪 | a+? | a | 尽量少匹配 |
| 占有(部分语言) | a++ | aaa | 一次吞完,不回溯 |
占有量词(
++、*+、?+、{n,m}+)在 Java、PCRE 中可用,JavaScript 和 Python 默认不支持。它能避免灾难性回溯,提升性能。
边界与锚点
锚点不匹配具体字符,而是匹配「位置」。
| 锚点 | 含义 |
|---|---|
^ | 字符串开头(多行模式下匹配每行开头) |
$ | 字符串结尾(多行模式下匹配每行结尾) |
\b | 单词边界 |
\B | 非单词边界 |
^ 与 $
模式:^\d+ 以数字开头
"123abc" => ✅
"abc123" => ❌
模式:\d+$ 以数字结尾
"abc123" => ✅
"123abc" => ❌
模式:^\d+$ 整个字符串全是数字
"12345" => ✅
"12a45" => ❌
单词边界 \b
\b 是「单词字符 \w」与「非单词字符 \W」之间的位置,常用于精确匹配整个单词:
模式:\bcat\b
"cat" => ✅
"a cat." => ✅
"category" => ❌(cat 后面跟字母,不是边界)
"concat" => ❌(cat 前面是字母,不是边界)
\B 则相反,匹配「非边界」位置:
模式:\Bcat
"concat" => ✅(cat 前面是字母,属非边界)
"cat" => ❌(开头就是边界)
分组与捕获
捕获分组 (...)
圆括号将多个字符视为一个整体,并对匹配内容进行捕获(保存到分组中,可后续引用):
模式:ab+ 重复的是 b
"abbb" => ✅
模式:(ab)+ 重复的是 ab 这个整体
"ababab" => ✅
捕获的分组可以按编号取出。编号按左括号出现的顺序从 1 开始:
模式:(\d{4})-(\d{2})-(\d{2}) 日期提取
"2026-07-17"
分组1 => "2026"
分组2 => "07"
分组3 => "17"
非捕获分组 (?:...)
如果只想分组、不需要捕获,用 (?:...),避免占用分组编号,性能也更好:
模式:(?:https?|ftp)://\S+ 匹配协议 + URL
"http://example.com" => ✅
"https://example.com" => ✅
"ftp://files.example" => ✅
命名分组 (?<name>...)
给分组起名字,可读性更强(JavaScript 用 (?<name>...),Python 用 (?P<name>...)):
模式:(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
"2026-07-17"
year => "2026"
month => "07"
day => "17"
反向引用 \1
在模式内部引用前面捕获的内容,用 \1、\2……(命名分组可用 \k<name>)。常用于匹配成对出现的内容:
模式:(\w+)\s\1 单词后跟空格再跟相同的单词
"hello hello" => ✅
"hello world" => ❌
模式:(['"])[^'"]*\1 匹配成对引号包裹的字符串
"'hello'" => ✅
'"hello"' => ✅
"'hello\"" => ❌(引号不配对)
选择(分支)
| 表示「或」,在多个备选项中匹配任意一个:
模式:cat|dog|bird
"cat" => ✅
"dog" => ✅
"bird" => ✅
模式:yes|no
"yes" => ✅
"no" => ✅
| 的作用范围到最近的分组边界。注意结合优先级带来的坑:
模式:\bcat|dog\b
本意是「cat 或 dog 单词」,但实际是「单词边界+cat」或「dog+单词边界」
正确写法:\b(cat|dog)\b
"cat" => ✅
"dog" => ✅
"category" => ❌
用 (?:...) 包裹可以避免不想要的捕获:
模式:\b(?:cat|dog|bird)\b 匹配三种动物单词之一,不捕获
转义
用 \ 让元字符回归字面含义,或让普通字符获得特殊含义:
模式:\.\*\? 匹配字面的 ".*?"
"a.*?b" => ✅
模式:\(\d+\) 匹配括号包裹的数字
"(123)" => ✅
模式:C:\\Windows 匹配 Windows 路径
"C:\Windows" => ✅
常用需要转义的元字符一览:
. * + ? ^ $ { } [ ] ( ) | \
提示:在字符类
[...]内部,大部分元字符失去特殊含义,无需转义。例如[.]匹配字面点号,[*+?]匹配这三个字符之一。但在[]内仍需转义的有]、\、^(在开头时)、-(在中间表示范围时)。
断言(Lookaround)
断言「向某个方向看一眼」是否满足条件,但不消耗字符(不占用匹配结果)。
先行断言(Lookahead)
| 写法 | 名称 | 含义 |
|---|---|---|
(?=...) | 正向先行 | 右边必须匹配 |
(?!...) | 负向先行 | 右边必须不匹配 |
模式:\d+(?=元) 数字后面跟着「元」
"价格100元" => 匹配 "100"(不包含「元」)
模式:\d+(?!元) 数字后面不是「元」
"100元 200千克" => 匹配 "200"
模式:\b\w+(?!ing\b) 不以 ing 结尾的单词
后行断言(Lookbehind)
| 写法 | 名称 | 含义 |
|---|---|---|
(?<=...) | 正向后行 | 左边必须匹配 |
(?<!...) | 负向后行 | 左边必须不匹配 |
模式:(?<=¥)\d+ 前面是「¥」的数字
"¥100" => 匹配 "100"
模式:(?<!\$)\d+ 前面不是「$」的数字
"$100 200" => 匹配 "200"
模式:(?<=姓:)\S+ 提取「姓:」后面的内容
"姓名:张三 姓:李" => 匹配 "李"
后行断言在较新的浏览器和 Node 中已支持;部分老引擎(如 Safari 早期版本、Python 3.7 以前对变长后行有限制)可能不支持变长后行断言。
断言组合示例:密码强度
要求「8 位以上,含大写、小写、数字」:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
拆解:每个 (?=...) 都是从开头向右「扫一遍」检查是否包含某类字符,互不消耗位置,最后 .{8,} 限定总长度。
修饰符(标志)
修饰符写在模式末尾,改变整体匹配行为。
| 标志 | 含义 | 示例 |
|---|---|---|
g | 全局匹配(找出所有,而非第一个) | /a/g |
i | 忽略大小写 | /cat/i 匹配 Cat |
m | 多行模式,^/$ 匹配每行边界 | /^\d/gm |
s | dotAll,让 . 匹配换行符 | /a.b/s |
u | Unicode 模式,正确处理多字节字符 | /\u{1F600}/u |
y | 粘性匹配,从 lastIndex 精确匹配 | — |
示例:
模式:/cat/gi 全局 + 忽略大小写
"Cat cat CAT" => 匹配 "Cat"、"cat"、"CAT"
模式:/^\d/gm 多行模式下匹配每行开头的数字
"1 a\n2 b\n3 c" => 匹配 "1"、"2"、"3"
模式:/a.b/s dotAll,. 可跨行
"a\nb" => ✅
模式:/^.$/u 正确匹配单个 emoji
"😀" => ✅(不加 u 时 emoji 会被拆成两个码元,匹配失败)
常用正则示例
邮箱
^[\w.+-]+@[\w-]+(\.[\w-]+)+$
"user@example.com" => ✅
"first.last+tag@sub.example.com" => ✅
"no-at-sign" => ❌
"@example.com" => ❌
这是实用级简化版本。完全符合 RFC 5322 的邮箱正则极其冗长,日常开发用上面的版本即可,最终仍需发送验证邮件确认。
手机号(中国大陆)
^1[3-9]\d{9}$
"13812345678" => ✅
"12345678901" => ❌(第二位不在 3-9)
"1381234567" => ❌(位数不足)
URL
^https?://[\w.-]+(:\d+)?(/[\w./?=&%-]*)?$
"https://example.com" => ✅
"http://localhost:3000/api/v1" => ✅
"ftp://example.com" => ❌
IPv4 地址
^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$
"192.168.1.1" => ✅
"255.255.255.0" => ✅
"256.1.1.1" => ❌(256 超出范围)
"1.2.3" => ❌(只有三段)
日期 YYYY-MM-DD
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
"2026-07-17" => ✅
"2026-13-01" => ❌(月份越界)
"2026-07-32" => ❌(日期越界)
中文字符
[\u4e00-\u9fa5]+
"hello世界" => 匹配 "世界"
提取 HTML 标签内容
<(\w+)[^>]*>(.*?)<\/\1>
<div class="x">hello</div> => 分组1="div", 分组2="hello"
千分位格式化(替换)
模式:\B(?=(\d{3})+(?!\d))
替换为:,
"1234567890" => "1,234,567,890"
思路:在每个「后面恰好跟着若干组三位数字」的非边界位置插入逗号。这里巧妙组合了正向先行断言
(?=...)和负向先行断言(?!...)。
去除重复单词
模式:\b(\w+)\s+\1\b
替换为:$1
"the the cat" => "the cat"
不同语言中的用法
JavaScript
const re = /(\d{4})-(\d{2})-(\d{2})/;
const m = re.exec('2026-07-17');
console.log(m[0]); // "2026-07-17" 整体匹配
console.log(m[1]); // "2026" 分组1
// 替换
'2026-07-17'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1');
// => "17/07/2026"
// 命名分组
const re2 = /(?<year>\d{4})-(?<month>\d{2})/;
const m2 = re2.exec('2026-07');
console.log(m2.groups.year); // "2026"
// 全局匹配
'1a2b3'.match(/\d/g); // ["1", "2", "3"]
Python
import re
# 提取
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2026-07-17')
print(m.group(1)) # "2026"
# 命名分组(Python 用 (?P<name>...))
m2 = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', '2026-07')
print(m2.group('year')) # "2026"
# 全部匹配
re.findall(r'\d+', 'a1b22c333') # ['1', '22', '333']
# 替换
re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\3/\2/\1', '2026-07-17')
# => "17/07/2026"
注意:JavaScript 用
$1引用分组,Python 用\1(替换字符串中两者都接受数字引用,但写法习惯不同)。
Java / Rust 速览
// Java
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("2026-07-17");
boolean found = m.find();
// Rust (regex crate)
use regex::Regex;
let re = Regex::new(r"\d{4}-\d{2}-\d{2}")?;
let m = re.find("2026-07-17").unwrap();
Java、Rust 字符串字面量中
\需写成\\,所以正则里的\d在源码里是\\d。JavaScript 正则字面量/.../和 Python 原始字符串r"..."则无需双重转义。
常见陷阱与技巧
- 贪婪导致过度匹配:解析 HTML 时
<.*>会从第一个<一直吃到最后一个>,应改用<.*?>或更严谨的字符类<[^>]*>。 - 忘记锚定边界:用
\d+校验「纯数字」会放过"abc123",应写成^\d+$。 .不匹配换行:跨行提取时记得加s标志,或改用[\s\S]。- 字符类里的
-:[a-z]是范围,但[-a]或[a-]里的-在边缘位置就是字面横杠。 - 优先用非捕获分组:不需要引用的分组用
(?:...),既省内存又不打乱分组编号。 - 能用字符串方法就别用正则:判断固定前缀用
startsWith,简单包含用includes,正则留给它真正擅长的模式匹配。 - 避免灾难性回溯:形如
(a+)+的嵌套量词在输入不匹配时会指数级回溯,可用占有量词或拆分模式规避。 - 先测试再上线:善用 regex101、debuggex 等工具可视化调试,并补充边界用例。
速查表
| 语法 | 含义 |
|---|---|
. | 任意字符(除换行) |
\d \w \s | 数字 / 单词字符 / 空白 |
\D \W \S | 上述取反 |
[abc] [^abc] | 字符类 / 取反 |
[a-z] | 范围 |
* + ? | 0+ / 1+ / 0或1 |
{n} {n,} {n,m} | 精确 / 至少 / 范围 |
*? +? ?? | 非贪婪 |
^ $ | 开头 / 结尾 |
\b \B | 单词边界 / 非边界 |
(...) (?:...) | 捕获 / 非捕获分组 |
(?<n>...) | 命名分组 |
\1 | 反向引用 |
| `a | b` |
(?=...) (?!...) | 正向 / 负向先行断言 |
(?<=...) (?<!...) | 正向 / 负向后行断言 |
g i m s u | 全局 / 忽略大小写 / 多行 / dotAll / Unicode |
总结
正则表达式是一门「小而强」的模式语言,核心其实就四件事:字符类描述「匹配什么」,量词描述「匹配多少次」,分组与断言描述「如何组合与定位」,标志位描述「如何匹配」。
掌握这套语法后,90% 的文本处理场景都能从容应对。剩下的难点往往不在语法,而在把现实问题准确翻译成模式——这需要多练、多拆解、多用工具验证。建议从本文的常用示例出发,结合 regex101 逐步改造成自己业务需要的版本。