首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 實作全文搜尋與替換:從系統指令到原生最佳實踐

     編輯:PHP 綜合知識

前言:為何需要全文搜尋與替換?

在專案維護或內容遷移時,常需批量修改多個檔案中的特定字串(如更新版權年份、修正舊域名、統一術語)。早期 PHP 開發者常呼叫系統指令(如 grep、sed)來達成,但這帶來安全性與可攜性風險。本文將解析經典做法的缺失,並提供現代 PHP 原生的最佳解決方案。

經典做法回顧與風險分析

早期常見寫法使用 exec() 呼叫 grep -r 遞迴搜尋,再逐檔 fopen/fread/str_replace/fwrite 替換:

<?php
$oldword = 'test';
$newword = 'text';
$rootpath = '/path/to/dir';

exec("/bin/grep -r '$oldword' $rootpath", $results, $errorCode);

if ($errorCode) {
    if ($errorCode == 1) {
        echo "Possibly no files were found with $oldword in them<br>\n";
    }
    echo "OS Error: $errorCode<br>\n";
    echo "Check 'man errno' and count down<br>\n";
    echo "Usually paths/permissions<br>\n";
}

while (list(, $path) = each($results)) {
    $parts = explode(':', $path);
    $path = $parts[0];
    $fp = fopen($path, 'r') or print("Cannot read $path<br>\n");
    if ($fp) {
        $data = fread($fp, filesize($path));
        fclose($fp);
        $newdata = str_replace($oldword, $newword, $data);
        $fp = fopen($path, 'w') or print("Cannot write $path<br>\n");
        if ($fp) {
            fwrite($fp, $newdata);
            fclose($fp);
            echo $path, "<br>\n";
        }
    }
}
?>

主要缺失:

  • 命令注入風險:$oldword、$rootpath 若未嚴格過濾,攻擊者可注入任意指令(如 ; rm -rf /)。
  • 平台相依性:依賴 Linux grep,Windows 環境無法直接執行。
  • 編碼與二進位檔損壞:fread 讀取整個檔案可能耗盡記憶體;若檔案為圖片/二進位,str_replace 會破壞檔案結構。
  • 錯誤處理脆弱:僅檢查回傳碼,缺乏例外處理與記錄。

現代 PHP 原生最佳實踐(PHP 7.4+ / 8.x)

使用 RecursiveDirectoryIterator + RecursiveIteratorIterator 遞迴遍歷,搭配 file_get_contents/file_put_contents(支援串流與鎖定),並內建二進位檢測與備份機制。

<?php
declare(strict_types=1);

function searchAndReplace(string $directory, string $search, string $replace, array $options = []): array {
    $defaults = [
        'extensions' => ['php', 'html', 'css', 'js', 'txt', 'md', 'json', 'xml', 'yml', 'yaml'],
        'excludeDirs' => ['.git', 'node_modules', 'vendor', 'storage', 'cache', 'logs'],
        'dryRun' => false,
        'backup' => true,
        'caseSensitive' => true,
        'maxFileSize' => 10 * 1024 * 1024, // 10MB
    ];
    $opts = array_merge($defaults, $options);

    $results = ['modified' => [], 'errors' => [], 'skipped' => []];

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::LEAVES_ONLY
    );

    foreach ($iterator as $file) {
        // 排除目錄
        $pathParts = explode(DIRECTORY_SEPARATOR, $file->getPath());
        if (array_intersect($pathParts, $opts['excludeDirs'])) {
            continue;
        }

        // 檢查副檔名
        if (!in_array(strtolower($file->getExtension()), $opts['extensions'])) {
            $results['skipped'][] = ['file' => $file->getPathname(), 'reason' => 'extension_not_allowed'];
            continue;
        }

        // 檢查檔案大小
        if ($file->getSize() > $opts['maxFileSize']) {
            $results['skipped'][] = ['file' => $file->getPathname(), 'reason' => 'file_too_large'];
            continue;
        }

        $pathname = $file->getPathname();
        try {
            $content = file_get_contents($pathname);
            if ($content === false) {
                throw new RuntimeException("Unable to read file");
            }

            // 簡易二進位檢測:避免破壞圖片等檔案
            if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $content)) {
                $results['skipped'][] = ['file' => $pathname, 'reason' => 'binary_file_detected'];
                continue;
            }

            $newContent = $opts['caseSensitive']
                ? str_replace($search, $replace, $content)
                : str_ireplace($search, $replace, $content);

            if ($newContent !== $content) {
                if (!$opts['dryRun']) {
                    // 備份
                    if ($opts['backup']) {
                        copy($pathname, $pathname . '.bak.' . date('YmdHis'));
                    }
                    // 寫入(LOCK_EX 避免競態)
                    if (file_put_contents($pathname, $newContent, LOCK_EX) === false) {
                        throw new RuntimeException("Failed to write file");
                    }
                }
                $results['modified'][] = $pathname;
            }
        } catch (Throwable $e) {
            $results['errors'][] = ['file' => $pathname, 'message' => $e->getMessage()];
        }
    }

    return $results;
}

// 使用範例
$report = searchAndReplace(
    '/var/www/html/project',
    'old_domain.com',
    'new_domain.com',
    ['dryRun' => true, 'caseSensitive' => false] // 先跑 dryRun 確認
);

echo "Modified: " . count($report['modified']) . "\n";
echo "Errors: " . count($report['errors']) . "\n";
echo "Skipped: " . count($report['skipped']) . "\n";
foreach ($report['modified'] as $f) { echo "  - $f\n"; }

進階建議

  • 版本控制整合:執行前確認 Git 狀態乾淨,執行後自動 git diff 檢視變更。
  • 正則表達式支援:若需複雜模式替換,改用 preg_replace 並注意效能與 catastrophic backtracking。
  • 大檔案串流處理:超過 maxFileSize 時,改用 SplFileObject 逐行讀寫至暫存檔,最後 rename 覆蓋(原子操作)。
  • 平行化:利用 parallel extension 或 pcntl_fork 多進程處理大型專案。

結語

摒棄 exec() 呼叫系統指令,改用 PHP 內建的 SPL 迭代器與檔案操作函式,不僅消除命令注入風險,更獲得跨平台相容性、更細緻的錯誤控制與可測試性。記得永遠先以 dryRun = true 執行預演,確認無誤後再正式寫入。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved