PHP 正規表達式 (Regular Expression)

正規表達式是一種用於描述字串模式的語法,可以用來搜尋、比對和取代文字。PHP 使用 PCRE (Perl Compatible Regular Expressions) 函數庫,所有相關函數都以 preg_ 開頭。

preg_match()

檢查是否符合模式:

<?php
$pattern = '/hello/';
$text = 'hello world';

if (preg_match($pattern, $text)) {
    echo "找到了";
}

// 取得匹配結果
if (preg_match($pattern, $text, $matches)) {
    print_r($matches);
    // Array ( [0] => hello )
}
?>

preg_match_all()

找出所有匹配:

<?php
$pattern = '/\d+/';
$text = 'I have 3 apples and 5 oranges';

preg_match_all($pattern, $text, $matches);
print_r($matches[0]);
// Array ( [0] => 3, [1] => 5 )
?>

preg_replace()

取代匹配的內容:

<?php
$pattern = '/\s+/';
$replacement = ' ';
$text = 'hello    world';

$result = preg_replace($pattern, $replacement, $text);
echo $result;  // hello world

// 多個模式
$patterns = ['/\d+/', '/[a-z]+/'];
$replacements = ['#', '*'];
$result = preg_replace($patterns, $replacements, 'abc123');
?>

preg_split()

使用正規表達式分割字串:

<?php
$pattern = '/[\s,]+/';
$text = 'apple, banana  cherry';

$result = preg_split($pattern, $text);
print_r($result);
// Array ( [0] => apple, [1] => banana, [2] => cherry )
?>

常用模式修飾符

修飾符加在正規表達式的結尾斜線之後,用於改變匹配行為:

<?php
// i - 不區分大小寫
preg_match('/hello/i', 'HELLO');  // true

// m - 多行模式
preg_match('/^hello/m', "world\nhello");  // true

// s - 讓 . 匹配換行
preg_match('/a.b/s', "a\nb");  // true

// u - UTF-8 模式
preg_match('/中文/u', '這是中文');  // true
?>

捕獲群組

使用括號 () 可以將部分模式組成一個「群組」,匹配後可以分別取出各群組的內容:

<?php
$pattern = '/(\d{4})-(\d{2})-(\d{2})/';
$text = '日期是 2024-12-09';

if (preg_match($pattern, $text, $matches)) {
    echo "完整匹配:" . $matches[0];  // 2024-12-09
    echo "年:" . $matches[1];        // 2024
    echo "月:" . $matches[2];        // 12
    echo "日:" . $matches[3];        // 09
}

// 命名群組
$pattern = '/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/';

if (preg_match($pattern, $text, $matches)) {
    echo "年:" . $matches['year'];
    echo "月:" . $matches['month'];
    echo "日:" . $matches['day'];
}
?>

常用正規表達式

<?php
// Email
$email = '/^[\w\.-]+@[\w\.-]+\.[a-z]{2,}$/i';

// 手機號碼(台灣)
$phone = '/^09\d{8}$/';

// URL
$url = '/^https?:\/\/[\w\.-]+\.[a-z]{2,}/i';

// IP 地址
$ip = '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/';

// 中文字元
$chinese = '/[\x{4e00}-\x{9fa5}]+/u';
?>

回調取代

preg_replace_callback() 讓你可以用函數來處理每個匹配的結果,適合需要動態計算取代內容的情況:

<?php
$text = 'hello world';

$result = preg_replace_callback(
    '/\b\w+\b/',
    function($matches) {
        return ucfirst($matches[0]);
    },
    $text
);

echo $result;  // Hello World
?>

驗證範例

<?php
class Validator {
    public static function email(string $value): bool {
        return (bool) preg_match(
            '/^[\w\.-]+@[\w\.-]+\.[a-z]{2,}$/i',
            $value
        );
    }
    
    public static function phone(string $value): bool {
        return (bool) preg_match('/^09\d{8}$/', $value);
    }
    
    public static function password(string $value): bool {
        // 至少 8 字元,包含大小寫和數字
        return (bool) preg_match(
            '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/',
            $value
        );
    }
}

// 使用
if (Validator::email('test@example.com')) {
    echo "Email 有效";
}
?>

擷取資料

<?php
// 擷取 HTML 標籤內容
$html = '<h1>標題</h1><p>內容</p>';
preg_match_all('/<(\w+)>([^<]+)<\/\1>/', $html, $matches);
// $matches[1] = ['h1', 'p']
// $matches[2] = ['標題', '內容']

// 擷取連結
$html = '<a href="https://example.com">Link</a>';
preg_match('/href="([^"]+)"/', $html, $matches);
echo $matches[1];  // https://example.com
?>

效能注意事項

<?php
// 如果只是簡單搜尋,使用字串函數更快
strpos($text, 'hello');  // 比 preg_match('/hello/', $text) 快

// 避免災難性回溯
// ❌ 危險的模式
// preg_match('/^(a+)+$/', $text);

// ✅ 使用原子群組或獨佔量詞
// preg_match('/^(?>a+)+$/', $text);
?>