PHP PCRE
Regex functions, modifiers, common patterns, and catastrophic backtracking.
Key PointsRegex checks shape; it cannot parse HTML or nested structures.
Functions
| Function | Returns |
|---|---|
| preg_match | 匹配次数 0/1 |
| preg_match_all | 全部匹配 |
| preg_replace | 替换后的字符串 |
| preg_replace_callback | 用回调生成替换内容 |
| preg_split | 按模式切分 |
| preg_grep | 筛选数组中匹配的元素 |
| preg_quote | 转义正则元字符 |
| preg_last_error | 最后一次错误码 |
Basics
PHP
<?php
var_dump(preg_match('/^\d{3}-\d{4}$/', "123-4567"));
var_dump(preg_match('/^\d{3}-\d{4}$/', "12-4567"));
preg_match('/(\d{4})-(\d{2})-(\d{2})/', "2023-11-15", $m);
print_r($m);
print_r(preg_split('/[,\s]+/', "a, b c,d"));
echo preg_replace('/\s+/', " ", "a b c"), "\n";
echo preg_quote("a.b*c", "/"), "\n";Output
int(1)
int(0)
Array
(
[0] => 2023-11-15
[1] => 2023
[2] => 11
[3] => 15
)
Array
(
[0] => a
[1] => b
[2] => c
[3] => d
)
a b c
a\.b\*cModifiers
| Modifier | Effect |
|---|---|
| i | 忽略大小写 |
| m | 多行:^ $ 匹配每行首尾 |
| s | . 匹配换行 |
| u | UTF-8 模式(处理中文必加) |
| x | 忽略空白,可写注释 |
| U | 量词默认变懒惰 |
| D | $ 只匹配结尾,不匹配末尾换行前 |
PHP
<?php
echo preg_match('/abc/i', "ABC") ? "忽略大小写命中" : "未命中", "\n";
echo preg_match('/^b/m', "a\nb") ? "多行命中" : "未命中", "\n";
echo preg_match('/a.b/s', "a\nb") ? "跨行命中" : "未命中", "\n";
// 中文:不加 /u 会按字节匹配
echo preg_match('/^\p{Han}+$/u', "中文") ? "是纯中文" : "不是", "\n";
echo preg_match('/^\p{Han}+$/', "中文") ? "是纯中文" : "不是", "\n";Output
忽略大小写命中
多行命中
跨行命中
是纯中文
不是\p{Han} and \w behave entirely differently with /u. Without it PCRE works on bytes and \w matches ASCII only.Common patterns
| Pattern | Matches |
|---|---|
| /^1[3-9]\d{9}$/ | 中国大陆手机号 |
| /^[\w.+-]+@[\w-]+\.[\w.]+$/ | 邮箱(简化版) |
| /^\d{17}[\dXx]$/ | 18 位身份证 |
| /^[a-zA-Z0-9_]{6,18}$/ | 用户名 |
| /^\d{4}-\d{2}-\d{2}$/ | 日期形状 |
| /^#[0-9a-fA-F]{6}$/ | 十六进制颜色 |
Named groups and callbacks
PHP
<?php
preg_match('/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/', "2023-11-15", $m);
echo $m["year"] . "/" . $m["month"] . "/" . $m["day"], "\n";
$out = preg_replace_callback(
'/\[(\w+)\]/',
fn($m) => strtoupper($m[1]),
"hello [world] and [php]"
);
echo $out, "\n";
// 反向引用
echo preg_replace('/(\w+) \1/', "$1", "the the cat"), "\n";Output
2023/11/15
hello WORLD and PHP
the catCatastrophic backtracking
Nested quantifiers make attempts grow exponentially. (a+)+b against a long run of a pins the CPU.
PHP
<?php
$bad = '/(a+)+b/';
$input = str_repeat("a", 26) . "c";
$start = microtime(true);
$r = preg_match($bad, $input);
$ms = round((microtime(true) - $start) * 1000, 1);
echo $r === 0 ? "未命中" : "命中", "\n";
echo "耗时超过 " . ($ms > 100 ? "100ms(危险)" : "100ms(可接受)"), "\n";
echo preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR ? "触发回溯上限" : "未触发上限", "\n";Output
未命中
耗时超过 100ms(危险)
触发回溯上限User-controlled input feeding a regex is a DoS vector. Limit input length, lower
pcre.backtrack_limit, or rewrite the pattern.When not to use regex
- Parsing HTML/XML —— use DOM; regex cannot handle nesting
- Parsing JSON —— use
json_decode - Validating email/URL —— use
filter_var - Simple substring checks ——
str_containsis far faster
函数
| 函数 | 返回 |
|---|---|
| preg_match | 匹配次数 0/1 |
| preg_match_all | 全部匹配 |
| preg_replace | 替换后的字符串 |
| preg_replace_callback | 用回调生成替换内容 |
| preg_split | 按模式切分 |
| preg_grep | 筛选数组中匹配的元素 |
| preg_quote | 转义正则元字符 |
| preg_last_error | 最后一次错误码 |
基本用法
PHP
<?php
var_dump(preg_match('/^\d{3}-\d{4}$/', "123-4567"));
var_dump(preg_match('/^\d{3}-\d{4}$/', "12-4567"));
preg_match('/(\d{4})-(\d{2})-(\d{2})/', "2023-11-15", $m);
print_r($m);
print_r(preg_split('/[,\s]+/', "a, b c,d"));
echo preg_replace('/\s+/', " ", "a b c"), "\n";
echo preg_quote("a.b*c", "/"), "\n";Output
int(1)
int(0)
Array
(
[0] => 2023-11-15
[1] => 2023
[2] => 11
[3] => 15
)
Array
(
[0] => a
[1] => b
[2] => c
[3] => d
)
a b c
a\.b\*c修饰符
| 修饰符 | 作用 |
|---|---|
| i | 忽略大小写 |
| m | 多行:^ $ 匹配每行首尾 |
| s | . 匹配换行 |
| u | UTF-8 模式(处理中文必加) |
| x | 忽略空白,可写注释 |
| U | 量词默认变懒惰 |
| D | $ 只匹配结尾,不匹配末尾换行前 |
PHP
<?php
echo preg_match('/abc/i', "ABC") ? "忽略大小写命中" : "未命中", "\n";
echo preg_match('/^b/m', "a\nb") ? "多行命中" : "未命中", "\n";
echo preg_match('/a.b/s', "a\nb") ? "跨行命中" : "未命中", "\n";
// 中文:不加 /u 会按字节匹配
echo preg_match('/^\p{Han}+$/u', "中文") ? "是纯中文" : "不是", "\n";
echo preg_match('/^\p{Han}+$/', "中文") ? "是纯中文" : "不是", "\n";Output
忽略大小写命中
多行命中
跨行命中
是纯中文
不是\p{Han}、\w 在中文上的行为**完全取决于 /u**。不加 /u 时 PCRE 按字节处理,\w 只认 ASCII 字母数字下划线,中文全被判为非单词字符。常用模式
| 模式 | 匹配 |
|---|---|
| /^1[3-9]\d{9}$/ | 中国大陆手机号 |
| /^[\w.+-]+@[\w-]+\.[\w.]+$/ | 邮箱(简化版) |
| /^\d{17}[\dXx]$/ | 18 位身份证 |
| /^[a-zA-Z0-9_]{6,18}$/ | 用户名 |
| /^\d{4}-\d{2}-\d{2}$/ | 日期形状 |
| /^#[0-9a-fA-F]{6}$/ | 十六进制颜色 |
命名捕获与回调
PHP
<?php
preg_match('/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/', "2023-11-15", $m);
echo $m["year"] . "/" . $m["month"] . "/" . $m["day"], "\n";
$out = preg_replace_callback(
'/\[(\w+)\]/',
fn($m) => strtoupper($m[1]),
"hello [world] and [php]"
);
echo $out, "\n";
// 反向引用
echo preg_replace('/(\w+) \1/', "$1", "the the cat"), "\n";Output
2023/11/15
hello WORLD and PHP
the cat回溯爆炸
嵌套量词会让匹配尝试次数指数增长。(a+)+b 遇到一长串 a 就会卡死 CPU。
PHP
<?php
$bad = '/(a+)+b/';
$input = str_repeat("a", 26) . "c";
$start = microtime(true);
$r = preg_match($bad, $input);
$ms = round((microtime(true) - $start) * 1000, 1);
echo $r === 0 ? "未命中" : "命中", "\n";
echo "耗时超过 " . ($ms > 100 ? "100ms(危险)" : "100ms(可接受)"), "\n";
echo preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR ? "触发回溯上限" : "未触发上限", "\n";Output
未命中
耗时超过 100ms(危险)
触发回溯上限用户可控的正则输入就是 DoS 入口。 缓解办法:限制输入长度、设
pcre.backtrack_limit、或者改写正则((a+)+b 改成 a+b)。不该用正则的场景
- 解析 HTML / XML —— 用 DOM,正则处理不了嵌套
- 解析 JSON —— 用
json_decode - 校验邮箱 / URL —— 用
filter_var - 简单字符串判断 ——
str_contains比正则快得多