Skip to content

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 rabbit

preg_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

最佳实践

  1. 简单替换用 preg_replace:固定替换内容时不需要回调
  2. 动态替换用 preg_replace_callback:需要根据匹配内容计算替换值时
  3. 固定分隔符用 explode:不需要正则时优先用 explode
  4. 过滤空元素用 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('/&amp;/', '&', $text);
    $text = preg_replace('/&lt;/', '<', $text);
    $text = preg_replace('/&gt;/', '>', $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_greppreg_replace 的功能。它只返回匹配和替换成功的部分。

参考链接