文件系統遍歷是批量處理文件、構建文件索引、清理臨時文件的基礎操作。本文提供標準遞歸寫法、SPL 迭代器現代寫法、以及大目錄下的性能優化建議。
<?php
/**
* 遞歸遍歷目錄,返回所有文件完整路徑數組
* @param string $dir 起始目錄絕對路徑
* @param array $results 引用傳遞結果數組
* @return array
*/
function scanDirRecursive($dir, &$results = []) {
// 規範化路徑
$dir = rtrim($dir, '/\\');
if (!is_dir($dir) || !is_readable($dir)) {
trigger_error("目錄不可讀或不存在: $dir", E_USER_WARNING);
return $results;
}
$handle = opendir($dir);
if (!$handle) return $results;
while (($entry = readdir($handle)) !== false) {
// 跳過 . 和 ..
if ($entry === '.' || $entry === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_file($path)) {
$results[] = $path;
} elseif (is_dir($path)) {
// 遞歸進入子目錄
scanDirRecursive($path, $results);
}
// 注意:is_link() 情況下需決定是否跟隨鏈接,避免循環
}
closedir($handle);
return $results;
}
// 使用示例
$allFiles = scanDirRecursive('/var/www/html/uploads');
foreach ($allFiles as $file) {
echo $file . PHP_EOL;
}
?>
<?php
$dir = '/var/www/html/uploads';
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir,
RecursiveDirectoryIterator::SKIP_DOTS // 自動跳過 . 和 ..
| RecursiveDirectoryIterator::UNIX_PATHS // 路徑分隔符統一為 /
),
RecursiveIteratorIterator::SELF_FIRST // 先目錄後文件,或 CHILD_FIRST
);
// 過濾器:只獲取文件,排除隱藏文件、特定擴展名
$filter = new RegexIterator($iterator, '/^.+\.(php|js|css|html?)$/i', RegexIterator::MATCH);
foreach ($filter as $fileInfo) {
/** @var SplFileInfo $fileInfo */
echo "文件: {$fileInfo->getPathname()}\n";
echo "大小: {$fileInfo->getSize()} bytes\n";
echo "修改時間: " . $fileInfo->getMTime() . "\n";
echo "權限: " . substr(sprintf('%o', $fileInfo->getPerms()), -4) . "\n";
echo "---\n";
}
// 統計信息
echo "總文件數: " . iterator_count($filter) . "\n";
?>
SPL 優勢:內置跳過 ./..、支持正則過濾、可獲取 SplFileInfo 完整元數據、可控制遍歷順序、內存效率高 (迭代器模式)。
<?php
// 只掃描一級目錄下的 .php 文件
$files = glob('/var/www/html/*.php');
// 遞歸模式需 PHP 5.5+ GLOB_BRACE 不支持遞歸,需自行實現
?>
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 遞歸深度過大導致棧溢出 | PHP 默認棧限制、極深目錄結構 | 改用 SPL 迭代器 (內部用棧模擬) 或增加 ini_set('xdebug.max_nesting_level', 500) |
| 掃描超時 (max_execution_time) | 文件數十萬級,單次請求超時 | CLI 模式運行、分批處理、設置 set_time_limit(0)、生成緩存索引文件 |
| 內存耗盡 | 結果數組過大全部載入內存 | 使用 Generator (yield) 逐個產出、或直接處理不存數組、寫入臨時文件/數據庫 |
| 符號鏈接循環 | 软链接指向父目錄 | 記錄已訪問 inode ($fileInfo->getInode()),遇重複跳過 |
| 權限拒絕警告 | 無讀取權限的子目錄 | @opendir 抑制錯誤、is_readable() 預判、錯誤日誌記錄而非中斷 |
<?php
function scanDirGenerator($dir) {
$dir = rtrim($dir, '/\\');
$handle = @opendir($dir);
if (!$handle) return;
while (($entry = readdir($handle)) !== false) {
if ($entry === '.' || $entry === '..') continue;
$path = $dir . DIRECTORY_SEPARATOR . $entry;
if (is_file($path)) {
yield $path;
} elseif (is_dir($path)) {
yield from scanDirGenerator($path); // PHP 7+ 委托生成器
}
}
closedir($handle);
}
// 使用:逐個處理,內存常數級
foreach (scanDirGenerator('/data/logs') as $file) {
if (filesize($file) > 100 * 1024 * 1024) {
echo "大文件: $file\n";
}
}
?>
fscandir (PECL) 或系統命令 find /path -type f 配合 proc_open。