PCRE 正则语法
概述
PHP 使用 PCRE(Perl Compatible Regular Expressions)库来处理正则表达式。PCRE 提供了强大的模式匹配能力,支持元字符、量词、字符类、锚点、分组与捕获、反向引用等功能。本章系统讲解 PCRE 正则表达式的语法规则和常用模式。
基础概念
正则表达式的基本结构
PHP 中正则表达式以分隔符(delimiter)包围,最常用的分隔符是斜杠 /。模式可以附加修饰符(modifiers)。
/模式/修饰符分隔符选择
| 分隔符 | 示例 | 适用场景 |
|---|---|---|
/ | /pattern/i | 最常用 |
# | #pattern#i | 模式中含 / 时 |
~ | ~pattern~i | 模式中含 / 和 # 时 |
{} | {pattern}i | 代码风格匹配 |
PCRE 元字符速查
| 元字符 | 含义 | 示例 |
|---|---|---|
. | 匹配除换行符外的任意字符 | /a.c/ 匹配 abc、a1c |
^ | 字符串开头锚点 | /^Hello/ |
$ | 字符串结尾锚点 | /world$/ |
* | 前一项出现 0 次或多次 | /ab*c/ |
+ | 前一项出现 1 次或多次 | /ab+c/ |
? | 前一项出现 0 次或 1 次 | /ab?c/ |
\ | 转义字符 | /\./ 匹配 . |
| ` | ` | 或(alternation) |
() | 分组 | /(ab)+/ |
[] | 字符类 | /[aeiou]/ |
{n,m} | 量词(n 到 m 次) | /a{2,4}/ |
语法与代码
量词
php
<?php
declare(strict_types=1);
// 量词 - 控制前一项出现的次数
$patterns = [
'a*' => 'a 出现 0 次或多次(贪婪)',
'a+' => 'a 出现 1 次或多次(贪婪)',
'a?' => 'a 出现 0 次或 1 次',
'a{3}' => 'a 恰好出现 3 次',
'a{2,4}' => 'a 出现 2 到 4 次',
'a{2,}' => 'a 至少出现 2 次',
'a*?' => 'a 0 次或多次(非贪婪/懒惰)',
'a+?' => 'a 1 次或多次(非贪婪/懒惰)',
'a{2,4}?' => 'a 2 到 4 次(非贪婪/懒惰)',
];
// 贪婪 vs 非贪婪
$html = '<div>content</div><div>more</div>';
// 贪婪匹配(默认)- 匹配尽可能多
preg_match('/<div>.*<\/div>/', $html, $match);
echo $match[0]; // <div>content</div><div>more</div>(匹配到最远的 </div>)
// 非贪婪匹配 - 匹配尽可能少
preg_match('/<div>.*?<\/div>/', $html, $match);
echo $match[0]; // <div>content</div>(匹配到最近的 </div>)字符类
php
<?php
declare(strict_types=1);
// 字符类 [...] - 匹配方括号内任意一个字符
preg_match('/[aeiou]/', 'Hello', $match); // 匹配 e
preg_match('/[0-9]/', 'abc123', $match); // 匹配 1
preg_match('/[a-zA-Z]/', '123abc', $match); // 匹配 a
// 取反字符类 [^...] - 匹配不在方括号内的字符
preg_match('/[^0-9]/', '123abc', $match); // 匹配 a
// 字符类简写
$shorthands = [
'\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 ]', // 非空白字符
];
// 字符类中的特殊字符
preg_match('/[-]/', 'a-b', $match); // 连字符放在末尾或开头
preg_match('/[\]]/', 'a]b', $match); // 右括号需要转义
preg_match('/[\^]/', 'a^b', $match); // 脱字符不在开头时锚点
php
<?php
declare(strict_types=1);
// 锚点 - 匹配位置而非字符
// ^ 字符串开头
preg_match('/^Hello/', 'Hello World', $match); // 匹配
preg_match('/^World/', 'Hello World', $match); // 不匹配
// $ 字符串结尾
preg_match('/World$/', 'Hello World', $match); // 匹配
preg_match('/Hello$/', 'Hello World', $match); // 不匹配
// \b 单词边界
preg_match('/\bworld\b/', 'hello world', $match); // 匹配
preg_match('/\bworld/', 'helloworld', $match); // 不匹配(无边界)
// \B 非单词边界
preg_match('/\Borld/', 'helloworld', $match); // 匹配
// \A 和 \z(始终匹配字符串开头/结尾,不受 m 修饰符影响)
preg_match('/\AHello/', "Hello\nWorld", $match); // 匹配
preg_match('/World\z/', "Hello\nWorld", $match); // 匹配分组与捕获
php
<?php
declare(strict_types=1);
// 分组 (...) - 将多个字符组合为一个单元
// 捕获组 - 括号内的内容会被保存到 $matches 数组
// 基本捕获
preg_match('/(\d{4})-(\d{2})-(\d{2})/', '2024-01-15', $matches);
// $matches: ['2024-01-15', '2024', '01', '15']
// $matches[0]: 完整匹配
// $matches[1]: 第一个捕获组(年)
// $matches[2]: 第二个捕获组(月)
// $matches[3]: 第三个捕获组(日)
// 非捕获组 (?:...) - 分组但不捕获
preg_match('/(?:\d{4})-(\d{2})/', '2024-01-15', $matches);
// $matches: ['2024-01', '01']
// 只有一个捕获组
// 命名捕获组 (?P<name>...) - 使用名称访问
preg_match('/(?P<year>\d{4})-(?P<month>\d{2})/', '2024-01', $matches);
// $matches['year'] => '2024'
// $matches['month'] => '01'
// PHP 8.2+ 也支持 (?<name>...) 简写
// 嵌套捕获组
preg_match('/((\w+)\s(\w+))/', 'Hello World', $matches);
// $matches[0] => 'Hello World'
// $matches[1] => 'Hello World'(外层组)
// $matches[2] => 'Hello'(内层第一个)
// $matches[3] => 'World'(内层第二个)反向引用
php
<?php
declare(strict_types=1);
// 反向引用 \1, \2... 或 \g{1}, \g{2}...
// 引用前面捕获组匹配的内容
// 匹配重复单词
preg_match('/\b(\w+)\s+\1\b/', 'hello hello world', $matches);
// $matches[0] => 'hello hello'
// $matches[1] => 'hello'
// HTML 标签匹配
preg_match('/<(\w+)>(.*?)<\/\1>/', '<div>content</div>', $matches);
// $matches[0] => '<div>content</div>'
// $matches[1] => 'div'
// $matches[2] => 'content'
// 命名反向引用 (?P=name) 或 \k<name>
preg_match('/(?P<tag>\w+).*<\/(?P=tag)>/', '<div>content</div>', $matches);
// PHP 8.2+ 也支持 \k{name}或(Alternation)
php
<?php
declare(strict_types=1);
// 或 | - 匹配左边或右边的模式
preg_match('/cat|dog/', 'I have a dog', $match); // 匹配 dog
// 注意优先级:| 的优先级最低
preg_match('/cat|dogfood/', 'dogfood', $match); // 匹配 dog
// 等同于 (cat)|(dogfood)
// 使用分组控制优先级
preg_match('/(cat|dog)food/', 'catfood', $match); // 匹配 catfood详细说明
正则表达式优先级
从高到低的优先级排列:
- 转义字符
\ - 括号
()、[]、(?:)、(?=) - 量词
*、+、?、{n,m} - 序列和锚点
abc、^、$ - 或
|
写作建议
在编写正则表达式时,多用括号分组来明确优先级,避免依赖隐含的优先级规则,提高可读性。
转义规则
在正则表达式中,以下字符具有特殊含义,需要使用 \ 转义:
\ ^ $ . | ? * + ( ) [ ] { }在字符类 [] 中,需要转义的字符较少:]、\、^(在开头时)、-(在中间时)。
实战示例
邮箱验证
php
<?php
declare(strict_types=1);
function isValidEmail(string $email): bool
{
return (bool)preg_match(
'/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
$email
);
}
echo isValidEmail('user@example.com'); // true
echo isValidEmail('user.name+tag@co.jp'); // true
echo isValidEmail('invalid-email'); // falseIP 地址匹配
php
<?php
declare(strict_types=1);
function isValidIpv4(string $ip): bool
{
return (bool)preg_match(
'/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/',
$ip
);
}
echo isValidIpv4('192.168.1.1'); // true
echo isValidIpv4('256.1.1.1'); // false
echo isValidIpv4('10.0.0.1'); // true密码强度检查
php
<?php
declare(strict_types=1);
function checkPasswordStrength(string $password): string
{
$score = 0;
if (preg_match('/.{8,}/', $password)) {
$score++;
}
if (preg_match('/[a-z]/', $password)) {
$score++;
}
if (preg_match('/[A-Z]/', $password)) {
$score++;
}
if (preg_match('/\d/', $password)) {
$score++;
}
if (preg_match('/[!@#$%^&*()_+\-=\[\]{}|;:\'",.<>?\/]/', $password)) {
$score++;
}
return match ($score) {
0, 1 => 'very_weak',
2 => 'weak',
3 => 'medium',
4 => 'strong',
5 => 'very_strong',
};
}
echo checkPasswordStrength('abc'); // very_weak
echo checkPasswordStrength('Abc123'); // medium
echo checkPasswordStrength('Abc@123!'); // very_strong更多正则表达式模式
常用正则模式集合
php
<?php
declare(strict_types=1);
// 常用正则模式
$patterns = [
// 网络相关
'url' => '/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/',
'ipv4' => '/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/',
'email_simple' => '/^[^\s@]+@[^\s@]+\.[^\s@]+$/',
'mac_address' => '/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/',
// 文本处理
'html_tag' => '/<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/',
'html_comment' => '/<!--[\s\S]*?-->/',
'css_class' => '/\.([a-zA-Z_-][\w-]*)/',
'js_variable' => '/(?:var|let|const)\s+(\w+)/',
// 数据格式
'date_iso' => '/\d{4}-\d{2}-\d{2}/',
'time_24h' => '/\d{2}:\d{2}:\d{2}/',
'hex_color' => '/#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})/',
'uuid' => '/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i',
// 安全相关
'sql_injection' => '/(union|select|insert|update|delete|drop|alter|create|exec)/i',
'xss_pattern' => '/<script[\s\S]*?>[\s\S]*?<\/script>/i',
];非贪婪与贪婪选择指南
php
<?php
declare(strict_types=1);
// HTML 解析中的贪婪 vs 非贪婪
$html = '<div class="a">Content A</div><div class="b">Content B</div>';
// 贪婪 - 匹配到最后一个 </div>
preg_match('/<div.*>.*<\/div>/', $html, $match);
echo $match[0];
// <div class="a">Content A</div><div class="b">Content B</div>
// 非贪婪 - 匹配到第一个 </div>
preg_match('/<div.*?>.*?<\/div>/', $html, $match);
echo $match[0];
// <div class="a">Content A</div>
// 特定标签匹配(更精确)
preg_match('/<div class="a">.*?<\/div>/', $html, $match);
echo $match[0];
// <div class="a">Content A</div>
// 使用原子组避免过度回溯
// (?>...) 原子组 - 一旦匹配就不再回溯
preg_match('/<div>(?>[^<]*)<\/div>/', $html, $match);零宽断言(Lookaround)
php
<?php
declare(strict_types=1);
// 正向先行断言 (?=...)
// 匹配后面跟着特定内容的文本
preg_match_all('/\w+(?=\.)/', 'hello. world. test.', $matches);
// ['hello', 'world', 'test'](后面跟着 . 的单词)
// 正向后行断言 (?<=...)
// 匹配前面是特定内容的文本
preg_match_all('/(?<=\$)\d+/', 'price: $100, tax: $20', $matches);
// ['100', '20'](前面是 $ 的数字)
// 负向先行断言 (?!...)
// 匹配后面不跟特定内容的文本
preg_match_all('/\b(?!test)\w+\b/', 'hello test world demo test', $matches);
// ['hello', 'world', 'demo'](不是 test 的单词)
// 负向后行断言 (?<!...)
// 匹配前面不是特定内容的文本
preg_match_all('/(?<!\w)@(\w+)/', 'email: @user, text@domain.com', $matches);
// ['user'](前面不是单词字符的 @)零宽断言性能
零宽断言(lookaround)会增加正则引擎的复杂度,在性能敏感的场景中应谨慎使用。
注意事项
分隔符转义
如果模式中包含分隔符,必须使用 \ 转义。更好的做法是选择不同的分隔符。例如 URL 匹配用 # 而非 /。
回溯限制
PCRE 有回溯限制(pcre.backtrack_limit),默认为 1000000。过于复杂的正则表达式可能触发回溯限制,导致匹配失败。
最佳实践
- 使用非贪婪量词:除非确实需要贪婪匹配,否则使用
*?、+?、{n,m}? - 合理使用非捕获组:不需要捕获内容时使用
(?:...),提高性能 - 使用命名捕获组:
(?P<name>...)比数字索引更可读 - 选择合适的分隔符:避免过多的转义反斜杠
- 编写可读的正则:使用
x修饰符添加注释和空白 - 测试边界情况:对正则表达式进行充分的单元测试