正则表达式完全指北

· 11403

什么是正则表达式

正则表达式(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垂直制表符
\0NUL 字符
\xHH十六进制字符(如 \x41 = A
\uHHHHUnicode 字符(如 \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
sdotAll,让 . 匹配换行符/a.b/s
uUnicode 模式,正确处理多字节字符/\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+)+ 的嵌套量词在输入不匹配时会指数级回溯,可用占有量词或拆分模式规避。
  • 先测试再上线:善用 regex101debuggex 等工具可视化调试,并补充边界用例。

速查表

语法含义
.任意字符(除换行)
\d \w \s数字 / 单词字符 / 空白
\D \W \S上述取反
[abc] [^abc]字符类 / 取反
[a-z]范围
* + ?0+ / 1+ / 0或1
{n} {n,} {n,m}精确 / 至少 / 范围
*? +? ??非贪婪
^ $开头 / 结尾
\b \B单词边界 / 非边界
(...) (?:...)捕获 / 非捕获分组
(?<n>...)命名分组
\1反向引用
`ab`
(?=...) (?!...)正向 / 负向先行断言
(?<=...) (?<!...)正向 / 负向后行断言
g i m s u全局 / 忽略大小写 / 多行 / dotAll / Unicode

总结

正则表达式是一门「小而强」的模式语言,核心其实就四件事:字符类描述「匹配什么」,量词描述「匹配多少次」,分组与断言描述「如何组合与定位」,标志位描述「如何匹配」。

掌握这套语法后,90% 的文本处理场景都能从容应对。剩下的难点往往不在语法,而在把现实问题准确翻译成模式——这需要多练、多拆解、多用工具验证。建议从本文的常用示例出发,结合 regex101 逐步改造成自己业务需要的版本。