PHP 5.3 棄用、7.0 移除 POSIX 正則(ereg_*),強制遷移至 PCRE(preg_*)。本文從語法、語義、效能、Unicode、二進制安全、遷移工具六大維度深度對比,給出零風險遷移清單與自動化方案。
| 維度 | POSIX (ereg_*) | PCRE (preg_*) | 影響等級 |
|---|---|---|---|
| 分隔符/修飾符 | 無 | 必要 /pattern/flags | 🔴 破壞性 |
| 大小寫控制 | 函數名 ereg/eregi | i 修飾符 | 🔴 破壞性 |
| 貪婪模式 | 強制貪婪,不可關 | U 修飾符 / .*? 非貪婪 / .*+ 佔有優先 | 🟠 高風險 |
點號 . 匹配換行 | 是(含換行) | 否(需 s 修飾符) | 🟠 高風險 |
| 二進制安全 | 不安全(\0 截斷) | 安全 | 🔴 破壞性 |
| Unicode 支援 | 無 | u 修飾符完整支援 | 🔴 破壞性 |
| 前瞻/後顧/原子組 | 無 | 完整支援 | 🟢 新能力 |
| 效能 (NFA 引擎) | 基礎回溯 | 優化回溯 + JIT (PHP 7+) | 🟢 顯著提升 |
| 函數對應 | ereg/eregi | preg_match | 🔴 破壞性 |
ereg_replace/eregi_replace | preg_replace | 🔴 破壞性 | |
split/spliti | preg_split | 🔴 破壞性 |
// 原 POSIX:eregi('abc', $str) 不分大小寫
// 遷移錯誤:preg_match('/abc/', $str) 區分大小寫!
// 正確:preg_match('/abc/i', $str)
// 原:ereg('a.b', "a\nb") → true (POSIX . 匹配 \n)
// 遷移:preg_match('/a.b/', "a\nb") → false (PCRE . 不匹配 \n)
// 正確:preg_match('/a.b/s', "a\nb") 或 '/a[\s\S]b/'
// 原:ereg_replace('a.*b', '-', 'aXXXbYYYb') → '-YYYb' (貪婪匹配到最後b)
// 遷移:preg_replace('/a.*b/', '-', 'aXXXbYYYb') → '-YYYb' (同貪婪)
// 但若原代碼依賴貪婪特性,PCRE 需顯式控制:
// 非貪婪:'/a.*?b/' → '-YYYb' (匹配到第一個b)
// 佔有優先:'/a[^b]++b/' → '-YYYb' (零回溯)
$bin = "abc\x00def";
// ereg_replace('x', 'y', $bin) → 僅處理 "abc" (遇 \0 結束)
// preg_replace('/x/', 'y', $bin) → 完整處理
// split(',', ',a,b,') → ['', 'a', 'b', ''] (POSIX 保留首尾空)
// preg_split('/,/', ',a,b,') → ['', 'a', 'b', ''] (同)
// 但 split(':', 'a:b:') → ['a', 'b', ''] vs preg_split('/:/', 'a:b:') → ['a', 'b', '']
// PREG_SPLIT_NO_EMPTY 可移除空元素
$str = str_repeat('s', 32);
$n = 100000;
$t = microtime(true);
for ($i=0; $i<$n; $i++) preg_replace('/s/', '', $str);
echo "preg_replace: " . (microtime(true)-$t) . "s\n"; // ~0.42s
$t = microtime(true);
for ($i=0; $i<$n; $i++) ereg_replace('s', '', $str); // PHP 5.3- 下可用
echo "ereg_replace: " . (microtime(true)-$t) . "s\n"; // ~1.28s
$t = microtime(true);
for ($i=0; $i<$n; $i++) str_replace('s', '', $str);
echo "str_replace: " . (microtime(true)-$t) . "s\n"; // ~0.11s
結論:preg_* 比 ereg_* 快 3 倍;純字串操作用 str_* 再快 4 倍。
# 專案根目錄執行
grep -rn "ereg_\|split\(" --include="*.php" .
# 或使用 PHP_CodeSniffer
composer require squizlabs/php_codesniffer --dev
./vendor/bin/phpcs --standard=PHPCompatibility --runtime-set testVersion 7.0 .
// tests/RegexMigrationTest.php
public function testEregReplaceBehavior() {
$this->assertEquals('-YYYb', preg_replace('/a.*b/', '-', 'aXXXbYYYb')); // 驗證貪婪行為一致
$this->assertEquals('-', preg_replace('/a.*?b/', '-', 'aXXXbYYYb')); // 新增非貪婪測試
}
composer require rector/rector --dev
# 建立 rector.php
use Rector\Config\RectorConfig;
use Rector\PHPUnit\Set\PHPUnitSetList;
return RectorConfig::configure()
->withSets([PHPUnitSetList::PHP_UNIT_80])
->withRules([
\Rector\PHPUnit\Rector\FuncCall\EregToPregRector::class,
]);
# 執行
vendor/bin/rector process src --dry-run # 預覽
vendor/bin/rector process src # 執行
// 兼容層函數,僅供緊急過渡
function ereg_compat($pattern, $string, &$regs = null) {
return @preg_match('/' . str_replace('/', '\/', $pattern) . '/', $string, $regs);
}
function eregi_compat($pattern, $string, &$regs = null) {
return @preg_match('/' . str_replace('/', '\/', $pattern) . '/i', $string, $regs);
}
function ereg_replace_compat($pattern, $replacement, $string) {
return @preg_replace('/' . str_replace('/', '\/', $pattern) . '/', $replacement, $string);
}
// 注意:無法完美模擬 POSIX . 匹配換行、強制貪婪等語義
POSIX 正則已成歷史。遷移核心是:補分隔符、換修飾符、控貪婪、防二進制截斷、加 Unicode。善用 Rector 自動化、PHPCompatibility 掃描、單元測試守門,可將遷移風險降至最低。新專案嚴禁引入 ereg_*,直接擁抱 PCRE 強大生態。