preg_match / preg_match_all
概述
preg_match 和 preg_match_all 是 PHP 中执行正则表达式匹配的两个核心函数。preg_match 在找到第一个匹配后停止,preg_match_all 则查找所有匹配项。本章详细讲解这两个函数的用法、$matches 数组结构以及各种标志选项。
基础概念
函数签名对比
| 函数 | 返回值 | 匹配次数 |
|---|---|---|
preg_match | int|false | 找到第一个匹配即停止 |
preg_match_all | int|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 failedPREG_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。
最佳实践
- 优先使用命名捕获组:
(?P<name>...)比数字索引更可读 - preg_match_all 用 PREG_SET_ORDER:遍历匹配结果更方便
- 使用 PREG_UNMATCHED_AS_NULL:明确区分未匹配和空匹配
- 错误处理:始终检查返回值是否为
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'); // falsepreg_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 或优化正则模式(避免嵌套量词、使用原子组)。