PHP 提供多種目錄掃描方式,各有效能、記憶體、功能差異。本文詳細比較 opendir/readdir、scandir、glob、DirectoryIterator(SPL),並提供適用場景建議與最佳實踐。
function listDirStream(string $path): Generator { $handle = @opendir($path); if (!$handle) return; try { while (($entry = readdir($handle)) !== false) { if ($entry === '.' || $entry === '..') continue; yield $entry; // 惰性產生,適合超大目錄 } } finally { closedir($handle); } } // 使用:逐一處理,不將所有檔名載入記憶體 foreach (listDirStream('/var/log') as $file) { $full = '/var/log/' . $file; if (is_file($full) && filesize($full) > 10 * 1024 * 1024) { echo "大檔案: $file (" . filesize($full) . " bytes)\n"; } }
優點:記憶體常數級、可中途中斷、支援網路掛載點。
缺點:需手動管理 handle、無排序、無過濾。
// 預設升序,SCANDIR_SORT_DESCENDING 降序,SCANDIR_SORT_NONE 不排序(較快) $files = scandir('/etc', SCANDIR_SORT_NONE); $files = array_diff($files, ['.', '..']); // 移除特殊項目 // 進階:取得檔案資訊陣列 function scanDirInfo(string $path, int $sort = SCANDIR_SORT_ASCENDING): array { $entries = scandir($path, $sort); $result = []; foreach ($entries as $entry) { if ($entry === '.' || $entry === '..') continue; $full = $path . DIRECTORY_SEPARATOR . $entry; $result[] = [ 'name' => $entry, 'path' => $full, 'type' => is_dir($full) ? 'dir' : (is_file($full) ? 'file' : 'other'), 'size' => is_file($full) ? filesize($full) : null, 'mtime' => filemtime($full), 'perm' => substr(sprintf('%o', fileperms($full)), -4), ]; } return $result; }
優點:簡單、支援排序、回傳陣列易處理。
缺點:大目錄記憶體壓力大、無內建過濾。
// 取得所有 .php 檔案 $phpFiles = glob('/var/www/*.php'); // 非遞歸 $phpFiles = glob('/var/www/**/*.php', GLOB_BRACE); // PHP 5.3+ 支援 ** 遞歸(需 GLOB_BRACE) // 多模式 $files = glob('/var/www/*.{php,html,css,js}', GLOB_BRACE); // 取得目錄 $dirs = glob('/var/www/*', GLOB_ONLYDIR); // 進階:遞歸掃描特定副檔名 function globRecursive(string $pattern, int $flags = 0): array { $files = glob($pattern, $flags); $dirs = glob(dirname($pattern) . '/*', GLOB_ONLYDIR | $flags); foreach ($dirs as $dir) { $files = array_merge($files, globRecursive($dir . '/' . basename($pattern), $flags)); } return $files; } // 注意:glob 不支援超大目錄(記憶體),且 ** 遞歸在某些系統效能較差
優點:模式匹配強大、語法簡潔。
缺點:無排序控制、遞歸效能不穩、大目錄記憶體壓力大。
// 基礎迭代 $it = new DirectoryIterator('/etc'); foreach ($it as $fileInfo) { if ($fileInfo->isDot()) continue; echo $fileInfo->getFilename() . ' - ' . $fileInfo->getSize() . ' bytes - ' . date('Y-m-d H:i:s', $fileInfo->getMTime()) . "\n"; } // 遞歸迭代(含子目錄) $rit = new RecursiveIteratorIterator( new RecursiveDirectoryIterator('/var/www', FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($rit as $fileInfo) { $depth = $rit->getDepth(); $indent = str_repeat(' ', $depth); echo $indent . ($fileInfo->isDir() ? '[D] ' : '[F] ') . $fileInfo->getPathname() . "\n"; } // 進階過濾:只取 .php 檔案,大於 1KB $filter = new RegexIterator( new RecursiveIteratorIterator( new RecursiveDirectoryIterator('/var/www', FilesystemIterator::SKIP_DOTS) ), '/\.php$/i' ); foreach ($filter as $fileInfo) { if ($fileInfo->getSize() > 1024) { echo $fileInfo->getPathname() . ' (' . $fileInfo->getSize() . " bytes)\n"; } } // 自訂過濾器類別 class LargePhpFilter extends FilterIterator { public function accept(): bool { return $this->current()->isFile() && $this->current()->getExtension() === 'php' && $this->current()->getSize() > 1024; } } $it = new LargePhpFilter( new RecursiveIteratorIterator( new RecursiveDirectoryIterator('/var/www', FilesystemIterator::SKIP_DOTS) ) ); foreach ($it as $file) { echo $file->getPathname() . "\n"; }
優點:物件導向、豐富資訊方法(getSize(), getMTime(), getPerms(), isReadable()...)、支援過濾器鏈、記憶體效率好(迭代器模式)。
缺點:語法較繁瑣、學習曲線稍高。
| 方法 | 記憶體峰值 | 耗時 | 備註 |
|---|---|---|---|
| opendir + Generator | ~2 MB | 1.2 秒 | 流式處理,最省記憶體 |
| scandir | ~45 MB | 0.8 秒 | 一次性載入陣列 |
| glob (非遞歸) | ~30 MB | 0.9 秒 | 模式匹配開銷 |
| RecursiveDirectoryIterator | ~5 MB | 1.5 秒 | 物件建立開銷,但記憶體穩定 |
opendir + Generatorscandirglob(非遞歸)RecursiveDirectoryIterator + FilterIteratorSymfony Finder 或 League\Flysystem 封裝層class DirScanner { public static function scan( string $path, array $options = [] ): iterable { $opts = array_merge([ 'recursive' => false, 'pattern' => null, // glob 模式或 regex 'type' => 'all', // 'file' | 'dir' | 'all' 'sort' => 'name', // 'name' | 'size' | 'mtime' | 'none' 'order' => 'asc', // 'asc' | 'desc' 'min_depth' => 0, 'max_depth' => -1, // -1 無限制 'skip_dots' => true, 'follow_links' => false, 'as_array' => false, // true 回傳陣列,false 回傳 Generator ], $options); // 參數驗證 $path = rtrim(realpath($path), DIRECTORY_SEPARATOR); if (!$path || !is_dir($path)) { throw new InvalidArgumentException("無效目錄: $path"); } // 選擇迭代器 if ($opts['recursive']) { $iterator = new RecursiveDirectoryIterator( $path, FilesystemIterator::KEY_AS_PATHNAME | ($opts['skip_dots'] ? FilesystemIterator::SKIP_DOTS : 0) | ($opts['follow_links'] ? FilesystemIterator::FOLLOW_SYMLINKS : 0) ); $iterator = new RecursiveIteratorIterator( $iterator, RecursiveIteratorIterator::SELF_FIRST ); } else { $iterator = new DirectoryIterator($path); } // 包裝為 Generator 統一介面 $gen = function () use ($iterator, $opts) { $depth = 0; foreach ($iterator as $pathname => $fileInfo) { if ($opts['recursive']) { $depth = $iterator->getDepth(); if ($depth < $opts['min_depth'] || ($opts['max_depth'] >= 0 && $depth > $opts['max_depth'])) { continue; } } if ($opts['skip_dots'] && $fileInfo->isDot()) continue; $type = $fileInfo->isDir() ? 'dir' : ($fileInfo->isFile() ? 'file' : 'other'); if ($opts['type'] !== 'all' && $type !== $opts['type']) continue; // 模式匹配 if ($opts['pattern'] !== null) { $name = $fileInfo->getFilename(); if (strpos($opts['pattern'], '*') !== false || strpos($opts['pattern'], '?') !== false) { // glob 風格轉 regex $regex = '/^' . str_replace(['*', '?'], ['.*', '.'], preg_quote($opts['pattern'], '/')) . '$/i'; if (!preg_match($regex, $name)) continue; } else { // 直接 regex if (!preg_match($opts['pattern'], $name)) continue; } } yield [ 'name' => $fileInfo->getFilename(), 'path' => $fileInfo->getPathname(), 'relative' => substr($fileInfo->getPathname(), strlen($path) + 1), 'type' => $type, 'size' => $fileInfo->isFile() ? $fileInfo->getSize() : null, 'mtime' => $fileInfo->getMTime(), 'perm' => substr(sprintf('%o', $fileInfo->getPerms()), -4), 'depth' => $depth, ]; } }; $generator = $gen(); // 排序需收集到陣列 if ($opts['sort'] !== 'none') { $data = iterator_to_array($generator); usort($data, function ($a, $b) use ($opts) { $key = $opts['sort']; $cmp = $a[$key] <=> $b[$key]; return $opts['order'] === 'asc' ? $cmp : -$cmp; }); return $opts['as_array'] ? $data : (function () use ($data) { foreach ($data as $v) yield $v; })(); } return $opts['as_array'] ? iterator_to_array($generator) : $generator; } } // 使用範例 // 1. 遞歸取得所有 .log 檔案,按大小降序 $logs = DirScanner::scan('/var/log', [ 'recursive' => true, 'pattern' => '*.log', 'type' => 'file', 'sort' => 'size', 'order' => 'desc', 'as_array' => true ]); // 2. 非遞歸,只取目錄 $dirs = DirScanner::scan('/etc', ['type' => 'dir', 'as_array' => true]); // 3. 記憶體友善:Generator 逐一處理大目錄 foreach (DirScanner::scan('/huge/dir', ['recursive' => true, 'type' => 'file']) as $file) { if ($file['size'] > 100 * 1024 * 1024) { echo "大檔案: {$file['path']}\n"; } }