前言先说一件容易混淆的事PHP 里的「正则表达式」其实分两套历史一套是已经消失的 POSIX 扩展一套是现在唯一在用的 PCRE。POSIX 那套函数ereg()、eregi()、ereg_replace()、eregi_replace()、split()、spliti()、sql_regcase()在 PHP 5.3 就被废弃在 PHP 7.0 中被彻底移除。现在网上还能搜到大量讲ereg()的教程那些代码在 PHP 7 及以上版本里会直接报「Call to undefined function」。PHP 8 时代只认preg_*系列底层的 PCRE 引擎负责解析和执行。第二个常见误解是把正则当成「万能的格式校验器」。正则擅长的是在文本里找符合形状的片段不擅长判断语义是否成立。比如用正则去完整校验一个邮箱地址是否符合 RFC 规范是件既做不到又没必要的事——这件事应该交给filter_var($email, FILTER_VALIDATE_EMAIL)。分不清这两件事就会写出越来越长、越来越难维护的模式。本文是一份聚焦 PHP 侧的 PCRE 笔记定界符与修饰符、常用元字符、捕获组与断言以及两条必须小心的性能红线。一、定界符与修饰符一个 PHP 正则模式是字符串第一个字符是定界符最后一个字符是同一个定界符。定界符可以是任何非字母、非数字、非反斜杠、非空白的字符常用的是斜杠、井号和波浪号?php // 适用于 PHP 8.0$p1 /\d/;$p2 #\d#;$p3 ~\d~;var_dump(preg_match($p1, abc123)); // int(1)var_dump(preg_match($p2, abc123)); // int(1)var_dump(preg_match($p3, abc123)); // int(1)用井号做定界符的好处是处理路径、URL 这类含大量斜杠的文本时不必转义#^/var/log/#比/^\/var\/log\//好读得多。反过来模式里出现的定界符字符必须转义。定界符之后可以跟修饰符它们控制匹配行为修饰符作用i不区分大小写m多行模式行首行尾锚点按每行计算s让点号也匹配换行x扩展模式忽略模式里的空白与注释u按 UTF-8 解释模式与目标串U反转量词的贪婪性默认变懒惰A强制从头开始匹配D行尾锚点只匹配字符串真正的结尾S旧版本中用于预分析模式以加速匹配现代 PCRE 已不再需要关于u不加它时正则引擎把目标串当作字节序列点号匹配一个字节量词数的也是字节。中文字符在 UTF-8 下一个字占 3 个字节所以张三这种两个字的中文在没有u的模式下长度是 6。这一点造成的错误非常隐蔽?php // 适用于 PHP 8.0var_dump(preg_match(/^.{2,4}$/, 张三)); // int(0)按字节算有 6 个「字符」var_dump(preg_match(/^.{2,4}$/u, 张三)); // int(1)按字符算正好 2 个处理中文时请一律加上u并且要用 Unicode 属性转义如\p{Han}表示汉字、\p{L}表示任意语言的字母来描述字符类别不要指望\w会覆盖非 ASCII 字符。二、模式语法速查下面这份速查表覆盖了日常九成以上的需求. 除换行外的任意字符加 s 修饰符后含换行^ $ 行首 / 行尾加 m 后按行计算\d \D 数字 / 非数字\w \W 单词字符字母、数字、下划线/ 非单词字符\s \S 空白字符 / 非空白字符[abc] 字符类匹配其中任意一个[^abc] 反向字符类匹配不在其中的任意一个[a-z0-9] 字符区间* ? 0 次以上 / 1 次以上 / 0 或 1 次贪婪*? ? ?? 对应的懒惰版本尽量少匹配* ? 占有版本匹配后不回退{n} {n,} {n,m} 精确 n 次 / 至少 n 次 / n 到 m 次(...) 捕获组(?:...) 非捕获组只分组不占用编号(?Pname...) 命名捕获组等价的尖括号写法现代 PCRE 也支持\1 \kname 反向引用引用之前捕获到的内容(?...) 正向前瞻断言右侧必须符合但不消耗字符(?!...) 负向前瞻断言(?...) 正向后顾断言(?!...) 负向后顾断言a|b 分支匹配 a 或 b\b \B 单词边界 / 非单词边界\p{Han} 汉字需加 u 修饰符几点使用提示非捕获组能不用编号就别占用编号。(?:...)不产生捕获既省内存也让$matches的下标清晰。后顾断言要求定长或有限长度。这是 PCRE 的硬性限制带长度区间的后顾写法在部分场景下不被接受写之前先确认模式的定长性。x修饰符适合写长模式它忽略模式里的空白于是可以换行缩进、加注释可读性大幅提升。POSIX 时代的写法与现在的对应关系也整理一份方便迁移旧代码已移除的写法现在的等价写法ereg($p, $s)preg_match(/ . $p . /, $s)eregi($p, $s)preg_match(/ . $p . /i, $s)ereg_replace($p, $r, $s)preg_replace(/ . $p . /, $r, $s)split(,, $s)explode(,, $s)或preg_split(/,/, $s)sql_regcase()无对应需要时自行构造注意 POSIX 的字符类写法[[:alpha:]]、[[:digit:]]在 PCRE 里仍然可用它们和\w、\d不是一回事迁移时不用一起改掉。另外旧代码里常见的preg_replace()加e修饰符也在 PHP 5.5 被废弃、PHP 7.0 被移除正确做法是改用preg_replace_callback()。三、捕获组、断言与回溯preg_match()一次只找第一个匹配它的第三个参数是按引用传入的数组用来接收捕获结果。下标 0 是完整匹配1 开始依次是各捕获组?php // 适用于 PHP 8.0$pattern /^(\d{4})-(\d{2})-(\d{2})$/;$subject 2026-10-06;$result preg_match($pattern, $subject, $matches);var_dump($result); // int(1)echo $matches[1], 年, $matches[2], 月, $matches[3], 日, PHP_EOL;带名字的捕获组更好读但要注意它在结果数组里会出现两次——一次用名字做键一次用序号做键?php // 适用于 PHP 8.0$pattern /^(?year\d{4})-(?month\d{2})-(?day\d{2})$/;if (preg_match($pattern, 2026-10-06, $m) 1) {echo $m[year], -, $m[month], -, $m[day], PHP_EOL;echo $m[1], -, $m[2], -, $m[3], PHP_EOL; // 同一批值换个键取}断言assertion用来描述「匹配位置周围必须满足什么条件」但它不消耗字符所以不会出现在$matches里。它是替代「先匹配一大段再手工裁剪」的好工具?php // 适用于 PHP 8.0$text 价格 100 元折扣 20 元;// 只取后面紧跟「 元」的数字但不把「元」匹配进来preg_match_all(/\d(? 元)/, $text, $prices);echo implode(,, $prices[0]), PHP_EOL; // 100,20最后说性能。PCRE 使用回溯backtracking算法模式写不好会导致「回溯爆炸」引擎尝试了指数级的组合才判定失败CPU 被吃满。核心原因是嵌套量词——形如「一个量词修饰的组里又含量词」的模式在不匹配的长输入上代价极高。防御手段有三条能写精确就写精确例如限定内容的字符类和明确的长度范围。用占有量词*、、?告诉引擎「匹配到就不用回头」它对不需要回退的场景完全等价于贪婪量词。在进程层面设防pcre.backtrack_limit和pcre.recursion_limit这两个 ini 配置决定了回溯的上限超出后preg_*会返回false而不是一直算下去。第三条尤其重要因为回溯超限返回的是false不是0。如果代码写的是「没匹配上就报格式错误」回溯超限会被误报成「格式不对」把一个性能问题伪装成业务问题。?php // 适用于 PHP 8.0// 出错时用 preg_last_error_msg() 看原因需要 PHP 8.0PHP 7 用 preg_last_error() 拿错误码$ok preg_match(/\d/, abc);var_dump($ok); // int(0)真的没匹配上var_dump($ok false); // bool(false)说明这不是错误常见坑点❌ 用ereg()、eregi()、split()的旧代码直接上线。✅ 这些函数在 PHP 5.3 废弃、PHP 7.0 移除改用preg_match()、preg_match()加i修饰符、preg_split()或explode()。❌ 用preg_replace()的e修饰符执行替换结果。✅ 该修饰符 PHP 5.5 废弃、PHP 7.0 移除应改用preg_replace_callback()显式回调。❌ 处理中文时不加u修饰符。✅ 不加u时点号和量词都按字节算一个汉字占 3 字节长度判断会整体错位。❌ 用\w去匹配中文以为它会包含所有语言的字母。✅\w默认面向 ASCII匹配汉字请用\p{Han}匹配任意字母用\p{L}并且都要加u。❌ 把模式写进单引号字符串时忘了处理反斜杠。✅ 单引号里\d就是\d本身问题不大但\\d会变成字面量反斜杠加 d写模式时统一用单引号并只写一次反斜杠最不容易错。❌ 用正则去完整校验邮箱或日期的语义合法性。✅ 正则只适合做形状粗筛邮箱用filter_var()加FILTER_VALIDATE_EMAIL日期合法性用DateTime::createFromFormat()校验。❌ 写出嵌套量词的模式并用它去处理用户提交的长文本。✅ 嵌套量词容易引发回溯爆炸长输入下 CPU 飙高改用精确字符类、占有量词并给pcre.backtrack_limit设一个合理上限。❌ 把回溯超限返回的false当成「没匹配上」。✅false表示出错0才表示没匹配判断命中请写 1并在失败分支里检查preg_last_error()/preg_last_error_msg()。总结关注点结论现行扩展只有 PCRE 系的preg_*POSIX 的ereg_*在 PHP 7.0 已移除定界符任意非字母数字非反斜杠非空白的字符斜杠、井号、波浪号最常用必加修饰符处理中文一定要加u捕获组(...)捕获并占编号(?:...)只分组断言前瞻断言与后顾断言各自都有否定形式都不消耗字符错误与未命中出错返回false未命中返回0必须用区分性能红线嵌套量词会引发回溯爆炸靠精确模式、占有量词、回溯上限三重设防该交给别人的活邮箱、日期的语义校验不要用正则硬扛学 PHP 正则最省事的路径是先把「定界符 修饰符 u」这三件事形成肌肉记忆再记牢「false是错误、0是没匹配」这条判断规则剩下的语法细节随用随查即可。真正会让人栽跟头的从来不是记不住元字符而是把回溯爆炸和语义校验这两类问题错误地交给了正则。