Skip to content

preg_match / preg_match_all

概述

preg_matchpreg_match_all 是 PHP 中执行正则表达式匹配的两个核心函数。preg_match 在找到第一个匹配后停止,preg_match_all 则查找所有匹配项。本章详细讲解这两个函数的用法、$matches 数组结构以及各种标志选项。

基础概念

函数签名对比

函数返回值匹配次数
preg_matchint|false找到第一个匹配即停止
preg_match_allint|false查找全部匹配

语法与代码

preg_match 基本用法

php
<?php

declare(strict_types=1);

// preg_match - 匹配一次,返回匹配次数(0 或 1)
$pattern = '/(\d{4})-(\d{2})-(\d{2})/';
$subject = '日期: 2024-01-15 和 2023-12-25';

$count = preg_match($pattern, $subject, $matches);
echo $count;  // 1

print_r($matches);
// [0] => 2024-01-15     (完整匹配)
// [1] => 2024           (第 1 个捕获组)
// [2] => 01             (第 2 个捕获组)
// [3] => 15             (第 3 个捕获组)

preg_match_all 基本用法

php
<?php

declare(strict_types=1);

// preg_match_all - 匹配所有出现
$html = '<a href="https://example.com">Example</a> <a href="/about">About</a>';

// PREG_PATTERN_ORDER(默认)- 按模式排列
preg_match_all('/<a href="([^"]+)">([^<]+)<\/a>/', $html, $matches);
print_r($matches);
// [0] => ['<a href="https://example.com">Example</a>', ...]
// [1] => ['https://example.com', '/about']
// [2] => ['Example', 'About']

// PREG_SET_ORDER - 按匹配集合排列
preg_match_all('/<a href="([^"]+)">([^<]+)<\/a>/', $html, $matches, PREG_SET_ORDER);
// [0] => ['<a href="https://example.com">Example</a>', 'https://example.com', 'Example']
// [1] => ['<a href="/about">About</a>', '/about', 'About']

PREG_OFFSET_CAPTURE 标志

php
<?php

declare(strict_types=1);

// PREG_OFFSET_CAPTURE - 返回匹配在字符串中的偏移位置
$text = 'The price is $100 and $200 total';

preg_match_all('/\$(\d+)/', $text, $matches, PREG_OFFSET_CAPTURE);
// [0] => [['$100', 13], ['$200', 23]]
// [1] => [['100', 14], ['200', 24]]

// preg_match 也可以使用
preg_match('/\$(\d+)/', $text, $match, PREG_OFFSET_CAPTURE);
// [0] => ['$100', 13]
// [1] => ['100', 14]

命名捕获组

php
<?php

declare(strict_types=1);

// 命名捕获组 (?P<name>...) / (?<name>...)  PHP 8.2+ 简写
$log = '2024-01-15 10:30:45 [ERROR] Database connection failed';

preg_match(
    '/(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) \[(?P<level>\w+)\] (?P<message>.+)/',
    $log,
    $matches
);

echo $matches['date'];     // 2024-01-15
echo $matches['time'];     // 10:30:45
echo $matches['level'];    // ERROR
echo $matches['message'];  // Database connection failed

PREG_UNMATCHED_AS_NULL(PHP 7.2+)

php
<?php

declare(strict_types=1);

// 未匹配的捕获组返回 null 而非空字符串
$pattern = '/(a)|(b)/';
preg_match($pattern, 'b', $matches, PREG_UNMATCHED_AS_NULL);
// [0] => 'b'
// [1] => null(组 1 未匹配)
// [2] => 'b'(组 2 匹配)

// 不使用 PREG_UNMATCHED_AS_NULL(默认行为)
preg_match($pattern, 'b', $matches);
// [0] => 'b', [1] => '', [2] => 'b'

详细说明

$matches 数组结构

默认(PREG_PATTERN_ORDER):

$matches[0]  - 所有完整匹配
$matches[1]  - 第 1 个捕获组的所有匹配
$matches['name'] - 命名捕获组的所有匹配

PREG_SET_ORDER:

$matches[0]  - 第一次匹配的所有组
$matches[1]  - 第二次匹配的所有组

选择建议

需要遍历所有匹配时使用 PREG_SET_ORDER,每个元素是一个完整的匹配记录。需要按组处理时使用 PREG_PATTERN_ORDER

实战示例

提取 HTML 中的所有链接

php
<?php

declare(strict_types=1);

function extractLinks(string $html): array
{
    preg_match_all(
        '/<a\s+[^>]*href=["\']([^"\']*)["\'][^>]*>([^<]*)<\/a>/i',
        $html,
        $matches,
        PREG_SET_ORDER
    );

    return array_map(fn(array $m): array => [
        'url' => $m[1],
        'text' => trim($m[2]),
    ], $matches);
}

$html = '<a href="/home">首页</a><a href="/about">关于</a><a href="https://example.com">外部</a>';
print_r(extractLinks($html));

解析 Nginx 日志

php
<?php

declare(strict_types=1);

function parseNginxLog(string $logContent): array
{
    $pattern = '/(?P<ip>[\d.]+) - - \[(?P<time>[^\]]+)\] "(?P<method>\w+) (?P<path>[^ ]+) HTTP\/[\d.]+" (?P<status>\d+)/';

    preg_match_all($pattern, $logContent, $matches, PREG_SET_ORDER);

    return array_map(fn(array $m): array => [
        'ip' => $m['ip'],
        'time' => $m['time'],
        'method' => $m['method'],
        'path' => $m['path'],
        'status' => (int)$m['status'],
    ], $matches);
}

注意事项

preg_match 返回值

preg_match 返回 0 或 1(false 仅在错误时)。preg_match_all 返回匹配总数(0 或正整数)。

PHP 8.0+ 严格参数

PHP 8.0+ 传入非字符串参数会抛出 TypeError。

最佳实践

  1. 优先使用命名捕获组(?P<name>...) 比数字索引更可读
  2. preg_match_all 用 PREG_SET_ORDER:遍历匹配结果更方便
  3. 使用 PREG_UNMATCHED_AS_NULL:明确区分未匹配和空匹配
  4. 错误处理:始终检查返回值是否为 false

preg_match 的高级用法

php
<?php

declare(strict_types=1);

// offset 参数 - 从指定偏移开始搜索
$text = 'abc123def456';

preg_match('/\d+/', $text, $match);
echo $match[0];  // 123

preg_match('/\d+/', $text, $match, 0, 6);
echo $match[0];  // 456(从偏移 6 开始搜索)

// flags 参数组合
preg_match('/pattern/u', $text, $match, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE);

// 使用正则验证数据
function isValidUsername(string $username): bool
{
    return (bool)preg_match('/^[a-zA-Z][a-zA-Z0-9_]{2,19}$/', $username);
}

function isValidPhone(string $phone): bool
{
    return (bool)preg_match('/^1[3-9]\d{9}$/', $phone);
}

echo isValidUsername('alice_2024');  // true
echo isValidUsername('1user');       // false
echo isValidPhone('13800138000');    // true
echo isValidPhone('12345678901');    // false

preg_match_all 的高级用法

php
<?php

declare(strict_types=1);

// 提取所有 CSS 类名
$css = '.container .main { width: 100%; } .sidebar { width: 30%; }';
preg_match_all('/\.([a-zA-Z][\w-]*)/', $css, $matches);
$classes = array_unique($matches[1]);
// ['container', 'main', 'sidebar']

// 提取 YAML 键值对
$yaml = 'name: Alice
age: 30
email: alice@example.com';

preg_match_all('/^([a-zA-Z_]+):\s*(.+)$/m', $yaml, $matches, PREG_SET_ORDER);

$config = [];
foreach ($matches as $match) {
    $config[$match[1]] = trim($match[2]);
}
// ['name' => 'Alice', 'age' => '30', 'email' => 'alice@example.com']

// 提取所有数字范围
$text = 'ranges: 1-10, 20-30, 100-200';
preg_match_all('/(\d+)-(\d+)/', $text, $matches, PREG_SET_ORDER);

foreach ($matches as $match) {
    $start = (int)$match[1];
    $end = (int)$match[2];
    echo "Range: {$start} to {$end}\n";
}

错误处理与安全检查

php
<?php

declare(strict_types=1);

// 错误处理
function safeMatch(string $pattern, string $subject): array
{
    // 检查正则是否有效
    $warning = null;
    set_error_handler(function (int $errno, string $errstr) use (&$warning): bool {
        $warning = $errstr;
        return true;
    });

    preg_match($pattern, $subject, $matches);
    restore_error_handler();

    if ($warning !== null) {
        throw new \InvalidArgumentException("无效的正则表达式: {$warning}");
    }

    return $matches;
}

try {
    $result = safeMatch('/[/', 'test');
} catch (\InvalidArgumentException $e) {
    echo $e->getMessage();
}

// 使用 preg_last_error_msg() 检查(PHP 8.0+)
preg_match('/(?P<name>)/', 'test', $matches);
if (preg_last_error() !== PREG_NO_ERROR) {
    echo '正则错误: ' . preg_last_error_msg();
}

高效的多模式匹配

php
<?php

declare(strict_types=1);

// 多模式匹配 - 只需一次遍历
function detectContentType(string $text): string
{
    $patterns = [
        'html' => '/<(div|span|p|table|form|input)/i',
        'json' => '/^\s*\{.*\}\s*$/s',
        'xml' => '/<\?xml\s/i',
        'csv' => '/^([^,]+,){2,}/',
        'yaml' => '/^[a-zA-Z_]+:\s*.+$/m',
    ];

    foreach ($patterns as $type => $pattern) {
        if (preg_match($pattern, $text)) {
            return $type;
        }
    }

    return 'plain';
}

echo detectContentType('<div class="box">');   // html
echo detectContentType('{"key": "value"}');    // json
echo detectContentType('<?xml version="1.0"'); // xml
echo detectContentType('name: Alice\nage: 30'); // yaml

正则表达式调试技巧

php
<?php

declare(strict_types=1);

// 调试正则匹配 - 详细输出
function debugMatch(string $pattern, string $subject): void
{
    echo "Pattern: {$pattern}\n";
    echo "Subject: {$subject}\n";

    $count = preg_match_all($pattern, $subject, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);

    echo "Matches: {$count}\n";
    foreach ($matches as $i => $match) {
        echo "  Match {$i}:\n";
        echo "    Full: [{$match[0][0]}] at offset {$match[0][1]}\n";
        for ($j = 1; $j < count($match); $j++) {
            echo "    Group {$j}: [{$match[$j][0]}] at offset {$match[$j][1]}\n";
        }
    }
}

debugMatch('/(\d{4})-(\d{2})-(\d{2})/', 'Date: 2024-01-15 and 2023-12-25');

调试建议

使用 PREG_SET_ORDER | PREG_OFFSET_CAPTURE 组合标志,可以在调试时清楚地看到每个匹配的内容和位置。

回溯限制

如果正则表达式导致回溯限制错误,可以临时增加 pcre.backtrack_limit 或优化正则模式(避免嵌套量词、使用原子组)。

参考链接