PHP 沒有內建「搜尋檔案」單一函式,需自行組合目錄遍歷、模式比對、內容掃描。本文提供四種實用搜尋函式:遞迴找檔名、Glob 模式比對、遞迴列出所有檔案、檔案內容全文搜尋,並討論大目錄效能優化、記憶體控制、權限處理等實務細節。
<?php
/**
* 遞迴搜尋檔案
* @param string $directory 起始目錄
* @param string $filename 目標檔名(支援 basename 比對)
* @param bool $recursive 是否遞迴子目錄
* @return string|false 找到回傳完整路徑,否則 false
*/
function findFile($directory, $filename, $recursive = true) {
$directory = rtrim($directory, '/\\') . DIRECTORY_SEPARATOR;
$handle = @opendir($directory);
if (!$handle) return false;
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$path = $directory . $entry;
if (is_dir($path)) {
if ($recursive) {
$found = findFile($path, $filename, true);
if ($found !== false) {
closedir($handle);
return $found;
}
}
} elseif ($entry === $filename) {
closedir($handle);
return $path;
}
}
closedir($handle);
return false;
}
// 使用
$path = findFile('/var/www/html', 'config.php');
if ($path) echo "找到:$path";
else echo "不存在";
function findFileGen($dir, $filename) {
$dir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $file) {
if ($file->isFile() && $file->getFilename() === $filename) {
yield $file->getPathname();
}
}
}
// 取第一個
$first = findFileGen('/var/www', 'config.php')->current();
<?php
// 找所有 .php 檔
$phpFiles = glob('/var/www/html/**/*.php', GLOB_BRACE);
// 註:PHP 5.3+ 支援 ** 遞迴,否則需自行遞迴
// 更通用:自定遞迴 glob
function globRecursive($pattern, $flags = 0) {
$files = glob($pattern, $flags);
$dirs = glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT);
foreach ($dirs as $dir) {
$files = array_merge($files, globRecursive($dir . '/' . basename($pattern), $flags));
}
return $files;
}
$allPhp = globRecursive('/var/www/html/*.php');
print_r($allPhp);
| Flag | 用途 |
|---|---|
GLOB_BRACE | 支援 {a,b,c} 多模式,如 *.{php,html,css} |
GLOB_NOSORT | 不排序,較快 |
GLOB_ONLYDIR | 只回傳目錄 |
GLOB_NOESCAPE | 反斜線不轉義 |
GLOB_NOCHECK | 無符合時回傳模式本身 |
<?php
/**
* 遞迴列出所有檔案路徑
* @param string $dir
* @return Generator
*/
function listAllFiles($dir) {
$dir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $file) {
if ($file->isFile()) {
yield $file->getPathname();
}
}
}
// 使用:逐一處理,不佔記憶體
foreach (listAllFiles('/var/www/html') as $path) {
// 處理單一檔案
if (filesize($path) > 10 * 1024 * 1024) {
echo "大檔:$path\n";
}
}
<?php
/**
* 搜尋檔案內容包含關鍵字的檔案
* @param string $dir 起始目錄
* @param string $keyword 關鍵字
* @param array $extensions 副檔名白名單,如 ['php','txt','js']
* @param string $encoding 檔案編碼,預設 utf-8
* @return Generator 產出 [path, lineNumber, matchedLine]
*/
function searchInFiles($dir, $keyword, $extensions = [], $encoding = 'utf-8') {
$dir = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR;
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $file) {
if (!$file->isFile()) continue;
$ext = strtolower($file->getExtension());
if ($extensions && !in_array($ext, $extensions, true)) continue;
$path = $file->getPathname();
$handle = @fopen($path, 'r');
if (!$handle) continue; // 權限不足等
$lineNum = 0;
while (($line = fgets($handle)) !== false) {
$lineNum++;
// 編碼轉 UTF-8 搜尋
$searchLine = ($encoding !== 'utf-8') ? iconv($encoding, 'utf-8//IGNORE', $line) : $line;
if (stripos($searchLine, $keyword) !== false) {
yield [$path, $lineNum, rtrim($line)];
}
}
fclose($handle);
}
}
// 使用
foreach (searchInFiles('/var/www/html', 'TODO', ['php', 'js', 'css']) as [$path, $line, $content]) {
echo "$path:$line: $content\n";
}
function listFilesExclude($dir, $excludeDirs = ['vendor', '.git', 'node_modules', 'storage']) {
$exclude = array_flip($excludeDirs);
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $file) {
// 檢查路徑中是否包含排除目錄
$parts = explode(DIRECTORY_SEPARATOR, $file->getPathname());
if (array_intersect($parts, array_keys($exclude))) continue;
if ($file->isFile()) yield $file->getPathname();
}
}
function listFilesDepth($dir, $maxDepth = 3) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
$it->setMaxDepth($maxDepth);
foreach ($it as $file) {
if ($file->isFile()) yield $file->getPathname();
}
}
PHP 單執行緒,大量檔案處理可用:
pcntl_fork 多進程(CLI 環境)。exec("find ... -print0 | xargs -0 -P 4 php process.php") 外部平行。| 問題 | 解決 |
|---|---|
目錄極大(百萬檔)glob/scandir 記憶體爆掉 | 用 RecursiveDirectoryIterator + Generator 逐一產出 |
| 權限不足導致警告/中斷 | @fopen、@opendir 抑制警告,失敗靜默跳過;CLI 以足夠權限用戶執行 |
| 符號連結導致無限遞迴 | RecursiveDirectoryIterator::FOLLOW_SYMLINKS 預設關閉;需小心開啟 |
| 網路掛載目錄(NFS/SMB)極慢 | 避免在掛載點遞迴;改用本地索引(如 locate/mlocate) |
| 內容搜尋二進制檔案產生雜訊 | 副檔名白名單過濾;或檢測 is_binary(檢查 NUL 位元、高位元組比例) |
Laravel:
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
$files = File::allFiles(storage_path('app'));
$phpFiles = array_filter($files, fn($f) => $f->getExtension() === 'php');
// 或用 Finder
use Symfony\Component\Finder\Finder;
$finder = Finder::create()->files()->name('*.php')->in(storage_path('app'));
Symfony Finder 內建支援排除、深度、排序、內容搜尋,生產環境強烈建議直接用 Finder,勿重造輪子。
RecursiveIteratorIterator Generator。glob() 配合 GLOB_BRACE,深層遞迴用自訂 globRecursive。RecursiveDirectoryIterator + Generator,記憶體恆定。fgets + iconv 編碼統一 + Generator 產出匹配行。File,功能完整、效能優化、測試充分。掌握迭代器與 Generator 模式,即能優雅處理任意規模的檔案搜尋需求。