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

PHP 批量替換檔案內容:遞迴遍歷目錄與多種替換策略

     編輯:PHP 相關知識

PHP 批量檔案內容替換工具完整實作

專案遷移、編碼統一、關鍵字替換、版權年份更新等場景常需批量修改目錄下所有檔案。本文提供從簡單字串替換到正則替換、編碼轉換、BOM 移除的完整工具類別,支援乾跑模式、備份與進度回呼。

一、核心工具類別

<?php
class BatchFileReplacer {
    private $rootDir;
    private $extensions = ['php', 'html', 'htm', 'js', 'css', 'txt', 'xml', 'json', 'md', 'sql'];
    private $excludeDirs = ['.git', '.svn', 'node_modules', 'vendor', 'storage', 'cache', 'logs'];
    private $dryRun = false;
    private $backup = true;
    private $callback = null;
    private $stats = ['files' => 0, 'replaced' => 0, 'errors' => 0, 'skipped' => 0];

    public function __construct(string $rootDir) {
        $this->rootDir = rtrim($rootDir, '/\\');
    }

    // 設定要處理的副檔名
    public function setExtensions(array $exts) { $this->extensions = $exts; return $this; }
    // 設定排除目錄
    public function setExcludeDirs(array $dirs) { $this->excludeDirs = $dirs; return $this; }
    // 乾跑模式 (不實際寫入)
    public function setDryRun(bool $dry) { $this->dryRun = $dry; return $this; }
    // 是否備份原檔 (.bak)
    public function setBackup(bool $backup) { $this->backup = $backup; return $this; }
    // 進度回呼: function(string $file, int $replacements, string $action)
    public function setProgressCallback(callable $cb) { $this->callback = $cb; return $this; }

    // 簡單字串替換
    public function replaceString(string $search, string $replace): array {
        return $this->process(function($content) use ($search, $replace) {
            $count = 0;
            $newContent = str_replace($search, $replace, $content, $count);
            return [$newContent, $count];
        });
    }

    // 正則替換
    public function replaceRegex(string $pattern, string $replace): array {
        return $this->process(function($content) use ($pattern, $replace) {
            $newContent = preg_replace($pattern, $replace, $content, -1, $count);
            return [$newContent, $count];
        });
    }

    // 多組替換 (陣列 ['search'=>'replace', ...])
    public function replaceMultiple(array $pairs): array {
        return $this->process(function($content) use ($pairs) {
            $total = 0;
            foreach ($pairs as $search => $replace) {
                $content = str_replace($search, $replace, $content, $count);
                $total += $count;
            }
            return [$content, $total];
        });
    }

    // 編碼轉換 (如 GB2312 -> UTF-8)
    public function convertEncoding(string $from, string $to): array {
        return $this->process(function($content) use ($from, $to) {
            $newContent = @iconv($from, $to . '//IGNORE', $content);
            $changed = ($newContent !== false && $newContent !== $content);
            return [$newContent ?: $content, $changed ? 1 : 0];
        });
    }

    // 移除 UTF-8 BOM
    public function removeBom(): array {
        return $this->process(function($content) {
            $bom = "\xEF\xBB\xBF";
            if (strncmp($content, $bom, 3) === 0) {
                return [substr($content, 3), 1];
            }
            return [$content, 0];
        });
    }

    // 核心處理邏輯
    private function process(callable $transformer): array {
        $this->stats = ['files' => 0, 'replaced' => 0, 'errors' => 0, 'skipped' => 0];
        $this->traverse($this->rootDir);
        return $this->stats;
    }

    private function traverse(string $dir) {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            if (!$file->isFile()) continue;
            
            // 檢查排除目錄
            $path = $file->getPathname();
            $relPath = substr($path, strlen($this->rootDir) + 1);
            $skip = false;
            foreach ($this->excludeDirs as $exDir) {
                if (strpos($relPath, $exDir . DIRECTORY_SEPARATOR) === 0) {
                    $skip = true; break;
                }
            }
            if ($skip) { $this->stats['skipped']++; continue; }

            // 檢查副檔名
            $ext = strtolower($file->getExtension());
            if (!in_array($ext, $this->extensions)) { $this->stats['skipped']++; continue; }

            $this->stats['files']++;
            $content = @file_get_contents($path);
            if ($content === false) { $this->stats['errors']++; continue; }

            // 執行轉換
            list($newContent, $count) = $transformer($content);
            
            if ($count > 0) {
                $this->stats['replaced'] += $count;
                if (!$this->dryRun) {
                    // 備份
                    if ($this->backup) {
                        @copy($path, $path . '.bak');
                    }
                    // 寫入 (獨佔鎖定)
                    $fp = @fopen($path, 'wb');
                    if ($fp) {
                        flock($fp, LOCK_EX);
                        fwrite($fp, $newContent);
                        fclose($fp);
                    } else {
                        $this->stats['errors']++;
                    }
                }
                if ($this->callback) $this->callback($path, $count, $this->dryRun ? 'DRY-RUN' : 'REPLACED');
            } else {
                if ($this->callback) $this->callback($path, 0, 'UNCHANGED');
            }
        }
    }

    public function getStats(): array { return $this->stats; }
}
?>

二、使用範例

1. 替換專案中所有舊域名

<?php
require_once 'BatchFileReplacer.php';

$replacer = new BatchFileReplacer(__DIR__ . '/src')
    ->setExtensions(['php', 'html', 'js', 'css', 'env'])
    ->setExcludeDirs(['.git', 'vendor', 'storage'])
    ->setDryRun(true) // 先乾跑確認
    ->setProgressCallback(function($file, $count, $action) {
        if ($count > 0) echo "[$action] $file ($count 次)\n";
    });

$stats = $replacer->replaceMultiple([
    'old-domain.com' => 'new-domain.com',
    'https://old-domain.com' => 'https://new-domain.com',
    'OLD_API_KEY' => 'NEW_API_KEY',
]);

echo "乾跑完成: " . json_encode($stats) . "\n";

// 確認無誤後執行真實替換
$replacer->setDryRun(false);
$stats = $replacer->replaceMultiple([
    'old-domain.com' => 'new-domain.com',
    // ...
]);
echo "替換完成: " . json_encode($stats) . "\n";
?>

2. 批量移除 BOM 與統一編碼為 UTF-8

<?php
$replacer = new BatchFileReplacer(__DIR__)
    ->setExtensions(['php', 'html', 'css', 'js'])
    ->setProgressCallback(function($f, $c, $a) { if($c) echo "$a: $f\n"; });

// 先移除 BOM
$replacer->removeBom();

// 再轉換編碼 (針對已知為 GB2312 的檔案)
// 注意:iconv 可能誤判編碼,建議針對特定目錄執行
$replacer->convertEncoding('GB2312', 'UTF-8');
?>

3. 正則替換:統一資料庫連線寫法

<?php
$replacer = new BatchFileReplacer(__DIR__ . '/config')
    ->setExtensions(['php'])
    ->setDryRun(false);

$stats = $replacer->replaceRegex(
    '/mysql_connect\s*\(\s*[\'"]([^\'"]+)[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/',
    'new PDO("mysql:host=$1;dbname=$3", "$2", "$3")'
);
// 將舊式 mysql_connect 替換為 PDO (需人工檢查)
?>

三、命令列工具版 (CLI)

<?php
// batch_replace.php
#!/usr/bin/env php
<?php
require_once 'BatchFileReplacer.php';

$shortopts = "d:s:r:e:x:bnh";
$longopts = ["dir:", "search:", "replace:", "ext:", "exclude:", "backup", "dry-run", "no-backup", "help"];
$options = getopt($shortopts, $longopts);

if (isset($options['h']) || isset($options['help']) || !isset($options['d'], $options['s'], $options['r'])) {
    echo "用法: php batch_replace.php -d <目錄> -s <搜尋> -r <替換> [-e php,html] [-x vendor,logs] [--dry-run] [--no-backup]\n";
    exit(1);
}

$replacer = new BatchFileReplacer($options['d'])
    ->setExtensions(explode(',', $options['e'] ?? 'php,html,js,css,txt'))
    ->setExcludeDirs(explode(',', $options['x'] ?? '.git,vendor,node_modules,storage,cache'))
    ->setDryRun(isset($options['dry-run']))
    ->setBackup(!isset($options['no-backup']))
    ->setProgressCallback(function($f, $c, $a) { if($c) echo "[$a] $f ($c)\n"; });

$stats = $replacer->replaceString($options['s'], $options['r']);
echo "\n統計: " . json_encode($stats, JSON_UNESCAPED_UNICODE) . "\n";
?>

使用:php batch_replace.php -d /var/www/project -s "old.com" -r "new.com" --dry-run

四、效能與安全建議

  • 大檔案處理:超過 10MB 檔案建議改用串流讀寫 (fopen/fgets) 避免記憶體耗盡。
  • 二進位檔案跳過:擴充 isBinary() 檢測 (檢查 NULL 字元、高位元組比例) 自動跳過圖片、編譯檔等。
  • 版本控制整合:執行前先 git status 確認工作區乾淨,執行後 git diff 檢視變更。
  • 平行處理:大型專案可使用 pcntl_fork 或 parallel 擴充多進程處理。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved