preg_replace / preg_split
概述
preg_replace 用于基于正则表达式进行字符串替换,preg_split 用于基于正则表达式分割字符串。两者配合 preg_replace_callback 回调替换,构成了 PHP 正则表达式处理的核心工具链。
基础概念
函数对照表
| 函数 | 功能 | 返回值 |
|---|---|---|
preg_replace | 正则替换 | string|array|null |
preg_replace_callback | 回调正则替换 | string|array|null |
preg_replace_callback_array | 多模式回调替换 | string|array|null |
preg_split | 正则分割 | array|false |
语法与代码
preg_replace — 正则替换
php
<?php
declare(strict_types=1);
// 基本替换
$text = 'Hello World 2024';
echo preg_replace('/World/', 'PHP', $text); // Hello PHP 2024
// 替换所有匹配(默认)
echo preg_replace('/\d+/', 'NUM', 'a1b2c3'); // aNUMbNUMcNUM
// 限制替换次数 - $limit 参数
echo preg_replace('/\d+/', 'NUM', 'a1b2c3', 2); // aNUMbNUMc3
// $limit = -1 表示不限制(默认)
// 使用反向引用替换
$text = 'Hello World Hello PHP';
echo preg_replace('/(\w+) (\w+)/', '$2 $1', $text); // World Hello PHP Hello
// 使用 $0 或 $1 引用
$text = 'price: 100, tax: 10';
echo preg_replace('/(\d+)/', '[$0]', $text); // price: [100], tax: [10]
// 数组搜索和替换
$text = 'The quick brown fox';
echo preg_replace(['/quick/', '/brown/', '/fox/'], ['slow', 'white', 'rabbit'], $text);
// The slow white rabbitpreg_replace_callback — 回调替换
php
<?php
declare(strict_types=1);
// preg_replace_callback — 对每个匹配调用回调函数
// 数字转中文
$text = '订单金额: 1234 元,共 56 件';
$result = preg_replace_callback('/\d+/', function (array $matches): string {
$map = ['0'=>'零','1'=>'一','2'=>'二','3'=>'三','4'=>'四',
'5'=>'五','6'=>'六','7'=>'七','8'=>'八','9'=>'九'];
return strtr($matches[0], $map);
}, $text);
echo $result; // 订单金额: 一二三四 元,共 五六 件
// URL 自动转链接
$text = '访问 https://example.com 和 http://google.com 获取信息';
$result = preg_replace_callback(
'#https?://[^\s]+#',
fn(array $m): string => '<a href="' . htmlspecialchars($m[0]) . '">' . $m[0] . '</a>',
$text
);preg_split — 正则分割
php
<?php
declare(strict_types=1);
// 基本分割
$fruits = preg_split('/[,;|]/', 'apple,banana;cherry|date');
// ['apple', 'banana', 'cherry', 'date']
// 限制分割数量
$parts = preg_split('/,/', 'one,two,three,four,five', 3);
// ['one', 'two', 'three,four,five']
// PREG_SPLIT_NO_EMPTY - 过滤空元素
$parts = preg_split('/,/', 'one,,two,,three', -1, PREG_SPLIT_NO_EMPTY);
// ['one', 'two', 'three']
// PREG_SPLIT_DELIM_CAPTURE - 包含分隔符
$parts = preg_split('/(-)/', '2024-01-15', -1, PREG_SPLIT_DELIM_CAPTURE);
// ['2024', '-', '01', '-', '15']
// PREG_SPLIT_OFFSET_CAPTURE - 返回偏移位置
$parts = preg_split('/,/', 'apple,banana,cherry', -1, PREG_SPLIT_OFFSET_CAPTURE);
// [['apple', 0], ['banana', 6], ['cherry', 13]]
// 按空白分割
$words = preg_split('/\s+/', ' Hello World PHP ', -1, PREG_SPLIT_NO_EMPTY);
// ['Hello', 'World', 'PHP']详细说明
替换中的反向引用
| 引用语法 | 含义 | 示例 |
|---|---|---|
$0 或 \\0 | 完整匹配 | preg_replace('/(\d+)/', '[$0]') |
$1 或 \\1 | 第 1 个捕获组 | preg_replace('/(a)(b)/', '$2$1') |
${1}name | 消除歧义 | preg_replace('/(\d)/', 'num${1}x') |
preg_split vs explode
如果分隔符是固定字符串而非正则表达式,优先使用 explode(),性能更好。
$limit 参数
preg_replace 的 $limit = 0 表示不限制替换。preg_split 的 $limit = 0 表示只返回一个元素。
实战示例
模板变量替换引擎
php
<?php
declare(strict_types=1);
class TemplateEngine
{
public static function render(string $template, array $data): string
{
return preg_replace_callback(
'/\{\{(\w+)\}\}/',
fn(array $m): string => $data[$m[1]] ?? $m[0],
$template
);
}
}
$tpl = '你好 {{name}},你的年龄是 {{age}} 岁。';
echo TemplateEngine::render($tpl, ['name' => 'Alice', 'age' => 30]);
// 输出: 你好 Alice,你的年龄是 30 岁。HTML 清洗
php
<?php
declare(strict_types=1);
function sanitizeHtml(string $html): string
{
$html = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $html);
$html = preg_replace('/<!--.*?-->/s', '', $html);
$html = preg_replace('/\s{2,}/', ' ', $html);
return trim($html);
}
$dirty = '<div>Hello<!-- comment --><script>alert(1)</script> World</div>';
echo sanitizeHtml($dirty);
// <div>Hello World</div>preg_grep — 正则过滤数组
php
<?php
declare(strict_types=1);
// preg_grep - 返回匹配正则的数组元素
$files = ['index.php', 'style.css', 'app.js', 'test.php', 'readme.md'];
$phpFiles = preg_grep('/\.php$/', $files);
// ['index.php', 'test.php'](保留键名)
$phpFiles = preg_grep('/\.php$/', $files, PREG_GREP_INVERT);
// ['style.css', 'app.js', 'readme.md'](不匹配的)
// PREG_GREP_INVERT - 反向过滤
$noPhp = preg_grep('/\.php$/', $files, PREG_GREP_INVERT);
// 实际应用:过滤日志级别
$logs = ['[DEBUG] Start', '[INFO] Running', '[ERROR] Failed', '[DEBUG] End'];
$errors = preg_grep('/\[ERROR\]/', $logs);
// ['[ERROR] Failed']注意事项
性能考量
对于简单的固定字符串替换,使用 str_replace 而非 preg_replace。对于不需要正则的分割,使用 explode 而非 preg_split。
最佳实践
- 简单替换用 preg_replace:固定替换内容时不需要回调
- 动态替换用 preg_replace_callback:需要根据匹配内容计算替换值时
- 固定分隔符用 explode:不需要正则时优先用
explode - 过滤空元素用 PREG_SPLIT_NO_EMPTY:避免手动 array_filter
preg_replace_callback_array(PHP 7.0+)
php
<?php
declare(strict_types=1);
// preg_replace_callback_array — 不同模式使用不同回调
$subject = 'Hello 2024, score: 95.5, name: Alice';
$result = preg_replace_callback_array(
[
'/\d{4}/' => fn(array $m): string => '[' . $m[0] . '](year)',
'/\d+\.\d+/' => fn(array $m): string => '[' . $m[0] . '](float)',
'/[A-Z][a-z]+/' => fn(array $m): string => '[' . $m[0] . '](name)',
],
$subject
);
echo $result;
// [Hello](name) [2024](year), score: [95.5](float), name: [Alice](name)
// 实际应用:对不同类型的数据应用不同的格式化规则
$log = '2024-01-15 ERROR Database failed at line 42';
$formatted = preg_replace_callback_array(
[
'/\d{4}-\d{2}-\d{2}/' => fn(array $m): string => '**' . $m[0] . '**',
'/ERROR/' => fn(array $m): string => '❌ ' . $m[0],
'/WARN/' => fn(array $m): string => '⚠️ ' . $m[0],
'/line \d+/' => fn(array $m): string => '`' . $m[0] . '`',
],
$log
);preg_replace 的 $count 参数(PHP 8.0+)
php
<?php
declare(strict_types=1);
// PHP 8.0+ 通过第 5 个参数($count)获取替换次数
$text = 'apple orange apple banana apple';
$count = 0;
// 注意参数顺序:pattern, replacement, subject, limit, count
preg_replace('/apple/', 'grape', $text, -1, $count);
echo $count; // 3
// 限制替换并获取实际替换次数
preg_replace('/apple/', 'grape', $text, 2, $count);
echo $count; // 2
// 在旧版 PHP 中,count 通过第 4 参数传递(需传 limit)
// PHP 8.0+ 统一为第 5 参数,更清晰preg_split 高级用法
php
<?php
declare(strict_types=1);
// 解析 HTTP 头部
$headers = "Content-Type: application/json\r\nContent-Length: 1234\r\nCache-Control: no-cache";
$headerLines = preg_split('/\r?\n/', $headers, -1, PREG_SPLIT_NO_EMPTY);
$parsed = [];
foreach ($headerLines as $line) {
if (preg_match('/^([^:]+):\s*(.+)$/', $line, $match)) {
$parsed[trim($match[1])] = trim($match[2]);
}
}
// ['Content-Type' => 'application/json', 'Content-Length' => '1234', ...]
// 解析复杂分隔符
$data = "key1=value1;;key2=value2;;key3=value3";
$pairs = preg_split('/;;/', $data, -1, PREG_SPLIT_NO_EMPTY);
$result = [];
foreach ($pairs as $pair) {
[$key, $value] = explode('=', $pair, 2);
$result[$key] = $value;
}
// ['key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3']反向引用在替换中的陷阱
php
<?php
declare(strict_types=1);
// 在双引号中使用反向引用
$text = 'Hello World';
// 使用 $1(推荐)
echo preg_replace('/(\w+) (\w+)/', '$2 $1', $text); // World Hello
// 使用 \\1(也可以)
echo preg_replace('/(\w+) (\w+)/', '\\2 \\1', $text); // World Hello
// 使用 ${1}(消除歧义)
echo preg_replace('/(\d+)/', 'num${1}x', 'test 42'); // test num42x
// 注意:在双引号字符串中 $1 会被 PHP 解析为变量
// 推荐:在正则替换中使用单引号字符串
$replacement = '$2 $1'; // 单引号,$1 不会被 PHP 解析批量文本处理
php
<?php
declare(strict_types=1);
function normalizeText(string $text): string
{
// 统一换行符
$text = preg_replace('/\r\n|\r/', "\n", $text);
// 去除多余空白行(保留单个空行)
$text = preg_replace('/\n{3,}/', "\n\n", $text);
// 去除行尾空白
$text = preg_replace('/[ \t]+\n/', "\n", $text);
// HTML 实体解码(简化版)
$text = preg_replace('/&/', '&', $text);
$text = preg_replace('/</', '<', $text);
$text = preg_replace('/>/', '>', $text);
return trim($text);
}
$raw = "Hello\r\n\r\n\r\nWorld \n\nTest";
echo normalizeText($raw);
// "Hello\n\nWorld\n\nTest"更多实战示例
Markdown 简易解析器
php
<?php
declare(strict_types=1);
function parseMarkdown(string $md): string
{
// 标题
$md = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $md);
$md = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $md);
$md = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $md);
// 粗体和斜体
$md = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $md);
$md = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $md);
// 行内代码
$md = preg_replace('/`(.+?)`/', '<code>$1</code>', $md);
// 链接
$md = preg_replace(
'/\[(.+?)\]\((.+?)\)/',
'<a href="$2">$1</a>',
$md
);
// 段落
$md = preg_replace('/\n\n+/', '</p><p>', $md);
$md = '<p>' . $md . '</p>';
return $md;
}
$text = '# Hello
This is **bold** and *italic*.
Check [link](https://example.com)';
echo parseMarkdown($text);回调性能
preg_replace_callback 的回调函数在每次匹配时都会被调用。对于大量匹配(百万级),应考虑更高效的处理方式,如一次性替换所有简单情况,仅对复杂情况使用回调。
preg_filter
PHP 8.0+ 提供了 preg_filter 函数,结合了 preg_grep 和 preg_replace 的功能。它只返回匹配和替换成功的部分。