Skip to content

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

详细说明

正则表达式优先级

从高到低的优先级排列:

  1. 转义字符 \
  2. 括号 ()[](?:)(?=)
  3. 量词 *+?{n,m}
  4. 序列和锚点 abc^$
  5. |

写作建议

在编写正则表达式时,多用括号分组来明确优先级,避免依赖隐含的优先级规则,提高可读性。

转义规则

在正则表达式中,以下字符具有特殊含义,需要使用 \ 转义:

\ ^ $ . | ? * + ( ) [ ] { }

在字符类 [] 中,需要转义的字符较少:]\^(在开头时)、-(在中间时)。

实战示例

邮箱验证

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');          // false

IP 地址匹配

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。过于复杂的正则表达式可能触发回溯限制,导致匹配失败。

最佳实践

  1. 使用非贪婪量词:除非确实需要贪婪匹配,否则使用 *?+?{n,m}?
  2. 合理使用非捕获组:不需要捕获内容时使用 (?:...),提高性能
  3. 使用命名捕获组(?P<name>...) 比数字索引更可读
  4. 选择合适的分隔符:避免过多的转义反斜杠
  5. 编写可读的正则:使用 x 修饰符添加注释和空白
  6. 测试边界情况:对正则表达式进行充分的单元测试

参考链接