配合本站正则测试工具,从字符类讲到分组断言。
| 章节 | |
|---|---|
| 01 字符类与量词 | 2 小节 |
| 02 边界与锚点 | 3 小节 |
| 03 分组与引用 | 4 小节 |
| 04 贪婪与懒惰 | 1 小节 |
| 05 断言实战 | 4 小节 |
| 06 常用正则清单 | 4 小节 |
正则表达式是一套描述「什么样的字符串算匹配」的记号。一条规则:先想清楚要匹配什么,再动手写,边写边猜是所有正则问题的根源。
本课所有例子都可以直接贴进在线正则表达式测试里边改边看高亮结果,比在代码里跑快得多。
| 写法 | 匹配 | 备注 |
|---|---|---|
. | 除换行外的任意字符 | 开了 s(dotall) 修饰符后连换行也匹配 |
[abc] | a、b、c 中的一个 | 方括号里大部分元字符失去特殊含义 |
[^abc] | 除了这三个之外的任意一个 | ^ 只在方括号开头才表示取反 |
[a-z0-9] | 范围,字母或数字 | - 放在首尾则表示字面的减号 |
\d \D | 数字 / 非数字 | 等价于 [0-9] |
\w \W | 单词字符 / 非单词字符 | 等价于 [A-Za-z0-9_],注意包含下划线 |
\s \S | 空白 / 非空白 | 空格、制表、换行等 |
中文的匹配靠 Unicode 范围,常用 [\x{4e00}-\x{9fa5}](PHP 需要加 u 修饰符),JavaScript 里写成 [一-龥]。
需要匹配字面的元字符时要转义:. * + ? ^ $ ( ) [ ] { } \ 前面加反斜杠。比如匹配一个真正的点用 \.。
| 写法 | 次数 |
|---|---|
* | 0 次或多次 |
+ | 1 次或多次 |
? | 0 次或 1 次 |
{n} | 恰好 n 次 |
{n,} | 至少 n 次 |
{n,m} | n 到 m 次 |
量词作用在它前面的那一个单元上:可以是一个字符、一个字符类、一个分组。ab+ 匹配 abbb,(ab)+ 才匹配 ababab。
\d{3}-\d{4} 匹配 010-1234
[A-Za-z]\w{5,15} 字母开头、总长 6 到 16 的用户名
\s*=\s* 等号两边允许任意空白新手最容易写出的错误是 .*:它几乎匹配一切,看起来能跑,遇到稍微复杂的输入就全乱。能用具体字符类就别用 .,比如取引号里的内容,用 "[^"]*" 远比 ".*" 可靠。
锚点不匹配任何字符,只匹配「位置」。这是正则里最容易忽略、却最能解决实际问题的一类记号。
| 记号 | 含义 |
|---|---|
^ | 字符串开头(多行模式 m 下是每行开头) |
$ | 字符串结尾(多行模式下是每行结尾) |
\b | 单词边界,单词字符与非单词字符的交界处 |
\B | 非单词边界 |
\A \z | 整个字符串的绝对开头 / 绝对结尾(部分语言支持) |
preg_match('/\d{6}/', 'abc123456def'); // 1,匹配上了!
preg_match('/^\d{6}$/', 'abc123456def'); // 0,这才是「必须是 6 位数字」不加锚点的正则是「包含」的意思,加了才是「整体就是」。所有做格式校验的正则都必须首尾加锚点,漏了就等于没校验——这是实际项目里最常见的正则安全漏洞。
$ 还有个细节:多数实现里 $ 允许字符串末尾有一个换行符。要严格到底用 \z,或者在 PHP 里加 D 修饰符。
cat 匹配 concatenate 里的 cat
\bcat\b 只匹配独立的单词 cat做敏感词、关键词替换时,不加 \b 会把 class 里的 ass 也换掉。注意 \b 是按 \w 定义的,对中文不起作用(中文之间没有「非单词字符」的边界),中文分词得另想办法。
$log = "200 /a\n404 /b\n500 /c";
preg_match_all('/^(\d{3}) (.*)$/m', $log, $m); // m 修饰符:^$ 按行匹配常用修饰符一览:
| 修饰符 | 作用 |
|---|---|
i | 忽略大小写 |
m | 多行,^ $ 匹配每行首尾 |
s | 让 . 也匹配换行 |
u | 按 UTF-8 处理,处理中文必加 |
x | 忽略空白与注释,可以把长正则写成多行 |
g | 全局匹配(JavaScript;PHP 用 preg_match_all 代替) |
括号有两个作用:把多个字符当成一个单元(好让量词作用其上),以及捕获匹配到的内容供后续使用。
| 写法 | 说明 |
|---|---|
(...) | 捕获组,从 1 开始编号 |
(?:...) | 非捕获组,只分组不捕获,省内存也更清晰 |
(?<name>...) | 命名捕获组 |
\1 \2 | 反向引用,在正则内部引用前面捕获到的内容 |
$1 \1 | 在替换字符串里引用捕获组(语言不同写法不同) |
$s = '2026-09-28 12:30:45';
preg_match('/^(\d{4})-(\d{2})-(\d{2})/', $s, $m);
// $m[0] 整个匹配 2026-09-28
// $m[1] 2026 $m[2] 09 $m[3] 28
// 命名组,字段一多就该用它了,靠编号迟早数错
preg_match('/^(?<y>\d{4})-(?<mo>\d{2})-(?<d>\d{2})/', $s, $m);
echo $m['y']; // 2026const m = '2026-09-28'.match(/^(?<y>\d{4})-(?<mo>\d{2})-(?<d>\d{2})$/);
console.log(m.groups.y); // 2026// 把 2026-09-28 换成 2026年09月28日
echo preg_replace('/(\d{4})-(\d{2})-(\d{2})/', '$1年$2月$3日', '2026-09-28');
// 回调替换:需要对捕获内容做计算时用
echo preg_replace_callback('/\d+/', function ($m) {
return $m[0] * 2;
}, 'a1 b2 c3'); // a2 b4 c6反向引用是正则里少数「有记忆」的能力,用来要求前后一致:
(\w)\1 连续重复的字符,如 aa、11
<(\w+)>.*?</\1> 标签名前后一致的一对标签
(\b\w+\b)\s+\1 重复的单词,如 "the the"最后一个在校对文稿时特别好用。
分支用竖线分隔多个选项,配合非捕获组限定范围:
^(?:GET|POST|PUT|DELETE)\s
(?:jpe?g|png|gif|webp)$注意分支的作用范围是整个表达式,^abc|def$ 的意思是「以 abc 开头」或「以 def 结尾」,想表达「开头是 abc 或 def」必须写成 ^(?:abc|def)。
量词默认是贪婪的:先尽可能多地吃,吃不下再一点点吐回来(回溯)。在量词后加 ? 变成懒惰,先尽可能少地吃,不够再多要一点。
原文:<b>粗体</b>和<i>斜体</i>
<.+> 贪婪 → 匹配到 <b>粗体</b>和<i>斜体</i> 整个一大串
<.+?> 懒惰 → 匹配到 <b> 只到第一个 >
<[^>]+> 最优 → 同样只到第一个 >,而且不需要回溯| 贪婪 | 懒惰 | 独占(部分实现) |
|---|---|---|
* | *? | *+ |
+ | +? | ++ |
? | ?? | ?+ |
{n,m} | {n,m}? | {n,m}+ |
三者的区别在于回溯:贪婪会回溯,懒惰会向前扩张,独占则完全不回溯——匹配失败就直接失败。
某些正则在特定输入下,回溯次数会呈指数级增长,把 CPU 打满,这叫 ReDoS(正则拒绝服务)。典型的危险结构是嵌套量词:
(a+)+$ 对 "aaaaaaaaaaaaaaaaaaaaaaX" 会跑到天荒地老
(\s*\w+)*$ 同样危险
^(\w+\s?)*$ 看起来人畜无害,其实一样判别方法:一个位置能被多条路径匹配(比如 \s* 和 \w+ 都能匹配到同一段),且外面还套着量词,就有风险。
避免的办法:
. 和宽泛的 \w,减少歧义;[^x]* 就别用 .*?,前者不需要回溯;(a+)+ 直接写成 a+;pcre.backtrack_limit、Go 的 RE2 引擎天生没有回溯问题);在在线正则表达式测试里拿边界输入(超长串、全是同一个字符、结尾差一位)试一遍,比看着正则想象要靠谱。
断言(lookaround)匹配位置而不消耗字符,用来表达「前面/后面得是什么,但我不要它」。
| 写法 | 名称 | 含义 |
|---|---|---|
(?=...) | 正向先行 | 右边必须是 |
(?!...) | 负向先行 | 右边不能是 |
(?<=...) | 正向后行 | 左边必须是 |
(?<!...) | 负向后行 | 左边不能是 |
后行断言在多数语言里要求定长(PCRE 允许分支但每个分支定长);JavaScript 从 ES2018 起支持变长后行。
多个先行断言叠加,是「同时满足多个条件」的标准写法:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,20}$拆开看:四个 (?=...) 分别要求「存在小写」「存在大写」「存在数字」「存在符号」,它们都在字符串开头这个位置上验证,验证完位置没动;最后 .{8,20}$ 才真正去消耗字符、限定长度。
想直观看到一个密码算强算弱,用密码强度检测。
// 从右往左每三位插一个逗号:位置右边的数字个数是 3 的倍数,且右边还有数字
echo preg_replace('/(?<=\d)(?=(\d{3})+$)/', ',', '1234567'); // 1,234,567这条正则不匹配任何字符,只匹配位置,替换时就是在这些位置插逗号。
^(?!.*(?:admin|root)).*$ 不含 admin 和 root 的行
\b(?!0)\d{1,3}\b 不以 0 开头的 1-3 位数字
(?<!\$)\b\d+\.\d{2}\b 前面没有美元符号的两位小数从 nginx 日志里取出 IP、状态码和 URL:
$line = '1.2.3.4 - - [28/Sep/2026:03:12:44 +0800] "GET /college/ HTTP/1.1" 200 5120';
$re = '/^(?<ip>\S+).*?"(?<method>[A-Z]+)\s(?<path>\S+)[^"]*"\s(?<code>\d{3})\s(?<size>\d+)/';
preg_match($re, $line, $m);
// $m['ip']=1.2.3.4 $m['method']=GET $m['path']=/college/ $m['code']=200注意这里用 [^"]* 而不是 .*? 去跨过协议版本,对日志这种长行来说能省掉大量回溯。
下面这些可以直接拿去用,但请先理解再用:正则没有放之四海皆准的「标准答案」,业务对边界的要求不同,规则也该不同。
手机号(宽松,够用) ^1[3-9]\d{9}$
邮箱(够用版) ^[\w.+-]+@[\w-]+(\.[\w-]+)+$
URL ^https?://[\w.-]+(:\d+)?(/[^\s]*)?$
IPv4(严格,含范围检查) ^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$
日期 yyyy-mm-dd ^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
中文姓名 ^[\x{4e00}-\x{9fa5}·]{2,16}$
身份证(18 位,不含校验位) ^\d{17}[\dXx]$
正整数 ^[1-9]\d*$
金额(最多两位小数) ^\d+(\.\d{1,2})?$关于邮箱要说一句:完全符合 RFC 5322 的正则有几千个字符,实际项目里永远不要用它。用上面这种宽松版做格式提示,真正的验证靠发一封确认邮件——这才是唯一可靠的办法。
同样,手机号的号段年年在变,正则写得越死,将来漏判的概率越大。
HTML 标签 <[^>]+>
标签内的文本 >([^<>]+)<
双引号里的内容 "([^"]*)"
Markdown 链接 \[([^\]]+)\]\(([^)]+)\)
UUID [0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}
十六进制颜色 #(?:[0-9a-fA-F]{3}){1,2}\b别用正则解析 HTML。标签可以嵌套、属性里可以有 >、注释里可以有标签,正则是正则语言,处理不了嵌套结构。抓取网页内容用解析器,或者直接用网页内容提取。同理,别用正则解析 JSON、XML 和完整的 URL——每种语言都有现成的库。
行首行尾空白 ^[ \t]+|[ \t]+$ (配合 m 修饰符)
连续空行压成一行 \n{3,} → \n\n
全角空格与不可见字符 [\x{3000}\x{00a0}\x{200b}]
重复的单词 \b(\w+)\s+\1\b// PHP:注意分隔符和修饰符,中文必加 u
preg_match('/^1[3-9]\d{9}$/', $phone);
preg_split('/[\s,;]+/u', $text); // 按多种分隔符切// JavaScript:g 修饰符的 lastIndex 是有状态的,别把带 g 的正则复用在 test() 上
const re = /^1[3-9]\d{9}$/;
re.test(phone);
[...text.matchAll(/(\w+)=(\w+)/g)].map(m => [m[1], m[2]]);# grep 默认是基础正则,用 -E 开扩展、-P 开 PCRE(才支持 \d、断言)
grep -E '^(GET|POST) /api/' access.log
grep -P '(?<=status=)\d{3}' app.log -o更多现成表达式见常用正则速查库,按长度过滤文本行用在线文本指定长度过滤。写好的正则,务必在在线正则表达式测试里用真实数据和边界数据各跑一遍再上线。
本文由 ip438查询网 原创,正文中的命令与配置片段可直接取用。 内容会随实践持续修订,发现错漏欢迎反馈。