目錄遍歷是檔案處理的基礎操作:掃描上傳目錄、建立檔案索引、清理暫存檔、生成網站地圖。早期寫法使用 dir() + 遞迴函式,現代 PHP 推薦使用 SPL (Standard PHP Library) 迭代器,提供記憶體效率、鏈式操作、過濾、排序、非同步整合。本文從基礎遞迴進階到生產級最佳實踐。
<?php
function listFilesRecursive(string $path): array
{
$files = [];
$items = scandir($path);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath)) {
$files = array_merge($files, listFilesRecursive($fullPath));
} else {
$files[] = $fullPath;
}
}
return $files;
}
// 使用
$allFiles = listFilesRecursive('/var/www/uploads');
?>
缺點:遞迴深度受限於堆疊、一次載入所有路徑佔記憶體、無法中斷、難以過濾/排序。
<?php
$source = '/var/www/uploads';
$dirIter = new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS);
$iter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::SELF_FIRST);
// 只取檔案
$files = new RegexIterator($iter, '/^.+$/i', RegexIterator::GET_MATCH); // 這裡需配合 is_file()
// 正確做法:CallbackFilterIterator
$files = new CallbackFilterIterator($iter, fn($current, $key, $iterator) => $current->isFile());
foreach ($files as $file) {
echo $file->getPathname() . "\n";
// $file->getSize(), $file->getMTime(), $file->getPerms(), $file->getExtension()
}
?>
<?php
function walkFiles(string $path, array $extensions = []): Generator
{
$dirIter = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$iter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::LEAVES_ONLY);
$filter = new CallbackFilterIterator($iter, function($current) use ($extensions) {
if (!$current->isFile()) return false;
if ($extensions && !in_array(strtolower($current->getExtension()), $extensions)) return false;
return true;
});
foreach ($filter as $file) {
yield $file->getPathname() => [
'size' => $file->getSize(),
'mtime' => $file->getMTime(),
'perms' => $file->getPerms(),
'extension' => $file->getExtension(),
'basename' => $file->getBasename(),
];
}
}
// 使用:逐筆處理,不佔記憶體
foreach (walkFiles('/var/log', ['log', 'txt']) as $path => $info) {
if ($info['size'] > 100 * 1024 * 1024) { // >100MB
echo "大檔案: $path (" . ($info['size']/1024/1024) . " MB)\n";
}
}
?>
<?php
$path = '/var/www/project';
$iter = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
// 排除 .git、vendor、node_modules、.idea
$excludeDirs = ['.git', 'vendor', 'node_modules', '.idea', 'storage', 'bootstrap/cache'];
$iter = new CallbackFilterIterator($iter, function($current) use ($excludeDirs) {
foreach ($excludeDirs as $dir) {
if (str_contains($current->getPathname(), DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR)) {
return false;
}
}
return true;
});
// 只取 PHP 檔案
$iter = new RegexIterator($iter, '/\.php$/i');
// 按修改時間排序(需先收集到陣列,或使用自訂排序迭代器)
$files = iterator_to_array($iter);
uasort($files, fn($a, $b) => $b->getMTime() <=> $a->getMTime());
foreach ($files as $file) {
printf("%s %s %d bytes\n", date('Y-m-d H:i', $file->getMTime()), $file->getPathname(), $file->getSize());
}
?>
<?php
/**
* 目錄掃描服務
* 特點:流式處理、豐富過濾、進度回呼、多執行緒支援 (parallel)、結果聚合
*/
class DirectoryScanner
{
public function __construct(
private int $bufferSize = 8192,
private ?callable $progressCallback = null
) {}
/**
* 掃描目錄
* @param string $path
* @param array $options:
* - 'extensions' => array (允許副檔名)
* - 'exclude_dirs' => array (排除目錄名)
* - 'exclude_patterns' => array (Regex 排除路徑)
* - 'min_size' => int (最小位元組)
* - 'max_size' => int (最大位元組)
* - 'min_mtime' => int (最早修改時間戳)
* - 'max_mtime' => int (最晚修改時間戳)
* - 'follow_symlinks' => bool
* - 'max_depth' => int (最大遞迴深度)
* - 'limit' => int (最大回傳筆數)
* @return Generator path => info
*/
public function scan(string $path, array $options = []): Generator
{
$flags = FilesystemIterator::SKIP_DOTS;
if ($options['follow_symlinks'] ?? false) $flags |= FilesystemIterator::FOLLOW_SYMLINKS;
$dirIter = new RecursiveDirectoryIterator($path, $flags);
$iter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::LEAVES_ONLY);
// 深度限制
if (isset($options['max_depth'])) {
$iter = new CallbackFilterIterator($iter, function($current) use ($options) {
return $current->getDepth() <= $options['max_depth'];
});
}
// 目錄排除
if (!empty($options['exclude_dirs'])) {
$exclude = $options['exclude_dirs'];
$iter = new CallbackFilterIterator($iter, function($current) use ($exclude) {
$pathname = $current->getPathname();
foreach ($exclude as $dir) {
if (str_contains($pathname, DIRECTORY_SEPARATOR . $dir . DIRECTORY_SEPARATOR)) return false;
}
return true;
});
}
// Regex 排除
if (!empty($options['exclude_patterns'])) {
$patterns = $options['exclude_patterns'];
$iter = new CallbackFilterIterator($iter, function($current) use ($patterns) {
foreach ($patterns as $pattern) {
if (preg_match($pattern, $current->getPathname())) return false;
}
return true;
});
}
// 檔案屬性過濾
$iter = new CallbackFilterIterator($iter, function($current) use ($options) {
if (!$current->isFile()) return false;
if (isset($options['extensions']) && !in_array(strtolower($current->getExtension()), $options['extensions'])) return false;
if (isset($options['min_size']) && $current->getSize() < $options['min_size']) return false;
if (isset($options['max_size']) && $current->getSize() > $options['max_size']) return false;
if (isset($options['min_mtime']) && $current->getMTime() < $options['min_mtime']) return false;
if (isset($options['max_mtime']) && $current->getMTime() > $options['max_mtime']) return false;
return true;
});
$count = 0;
$limit = $options['limit'] ?? PHP_INT_MAX;
foreach ($iter as $file) {
if ($count++ >= $limit) break;
$info = [
'path' => $file->getPathname(),
'size' => $file->getSize(),
'mtime' => $file->getMTime(),
'perms' => $file->getPerms(),
'extension' => $file->getExtension(),
'basename' => $file->getBasename(),
'relative' => substr($file->getPathname(), strlen($path) + 1),
];
if ($this->progressCallback) {
($this->progressCallback)($info, $count);
}
yield $file->getPathname() => $info;
}
}
/**
* 併行掃描多個目錄 (需 parallel 擴充)
*/
public function scanParallel(array $paths, array $options = []): Generator
{
if (!extension_loaded('parallel')) {
throw new RuntimeException("需要 parallel 擴充: pecl install parallel");
}
$runtime = new \parallel\Runtime();
$futures = [];
foreach ($paths as $path) {
$futures[$path] = $runtime->run(function($path, $options) {
$scanner = new self();
return iterator_to_array($scanner->scan($path, $options));
}, [$path, $options]);
}
foreach ($futures as $path => $future) {
$results = $future->value();
foreach ($results as $filePath => $info) {
yield $filePath => $info;
}
}
}
/**
* 聚合統計
*/
public function getStats(string $path, array $options = []): array
{
$stats = ['files' => 0, 'dirs' => 0, 'bytes' => 0, 'extensions' => []];
$dirIter = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$iter = new RecursiveIteratorIterator($dirIter, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iter as $item) {
if ($item->isDir()) {
$stats['dirs']++;
} elseif ($item->isFile()) {
$stats['files']++;
$stats['bytes'] += $item->getSize();
$ext = strtolower($item->getExtension());
$stats['extensions'][$ext] = ($stats['extensions'][$ext] ?? 0) + 1;
}
}
arsort($stats['extensions']);
return $stats;
}
}
?>
<?php
$scanner = new DirectoryScanner(progressCallback: fn($info, $i) => $i % 100 === 0 && echo "已掃描 $i 個檔案\r");
foreach ($scanner->scan('/var/www/uploads', [
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
'min_size' => 10 * 1024 * 1024,
'exclude_dirs' => ['thumbnails', 'temp'],
]) as $path => $info) {
echo "大圖: $path (" . round($info['size']/1024/1024, 2) . " MB)\n";
}
?>
<?php
$cutoff = strtotime('-30 days');
$deleted = 0;
foreach (new DirectoryScanner()->scan('/tmp/myapp', [
'max_mtime' => $cutoff,
'exclude_dirs' => ['.git'],
]) as $path => $info) {
if (unlink($path)) {
$deleted++;
echo "刪除: $path\n";
}
}
echo "共刪除 $deleted 個檔案。\n";
?>
<?php
$scanner = new DirectoryScanner();
$urls = [];
foreach ($scanner->scan('/var/www/html', [
'extensions' => ['php', 'html'],
'exclude_dirs' => ['vendor', 'node_modules', '.git'],
'exclude_patterns' => ['/\\.(php|html)$/'], // 排除隱藏檔
]) as $path => $info) {
$relative = $info['relative'];
$url = 'https://example.com/' . str_replace('.php', '', $relative);
$lastmod = date('Y-m-d', $info['mtime']);
$urls[] = compact('url', 'lastmod');
}
$sitemap = '' . "\n";
$sitemap .= '' . "\n";
foreach ($urls as $u) {
$sitemap .= " \n {$u['url']} \n {$u['lastmod']} \n \n";
}
$sitemap .= ' ';
file_put_contents('/var/www/html/sitemap.xml', $sitemap);
?>
| 方法 | 1萬檔案耗時 | 記憶體峰值 | 特點 |
|---|---|---|---|
| 遞迴 scandir() | ~1.2s | ~40MB | 全載入陣列、堆疊溢位風險 |
| RecursiveIteratorIterator | ~0.8s | ~2MB | 流式、可鏈式過濾 |
| Generator + CallbackFilterIterator | ~0.7s | ~1MB | 惰性求值、最低記憶體 |
| parallel 併行 (4核心) | ~0.25s | ~4MB | CPU 密集型加速、需擴充 |
| rsync/find 系統呼叫 | ~0.15s | ~0.5MB | 最快、但依賴外部指令、難以整合邏輯 |
目錄遍歷從簡易遞迴邁向 SPL 迭代器、生成器、併行處理,是 PHP 現代化的縮影。掌握 RecursiveDirectoryIterator、CallbackFilterIterator、Generator 組合技巧,能以極低記憶體、高擴充性解決各類檔案掃描需求。生產環境建議封裝為 DirectoryScanner 服務類,統一過濾邏輯、進度回呼、錯誤處理,並提供統計、併行等進階功能。