<?php
/**
* 遞歸統計目錄下文件總數(含子目錄)
* @param string $dir 目錄路徑
* @return int 文件數量
* @throws InvalidArgumentException 目錄不存在或不可讀
*/
function countFilesRecursive(string $dir): int
{
if (!is_dir($dir) || !is_readable($dir)) {
throw new InvalidArgumentException("目錄不存在或不可讀: $dir");
}
$count = 0;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$count++;
}
}
return $count;
}
// 使用範例
try {
$total = countFilesRecursive(__DIR__ . '/pic');
echo "圖片總數: $total"; // 輸出: 圖片總數: 100
} catch (Throwable $e) {
echo "錯誤: " . $e->getMessage();
}
?>
<?php
/**
* 進階文件計數器
* @param string $dir 目錄路徑
* @param array $options 選項:
* - 'extensions' => ['jpg','png'] 只統計指定副檔名(不含點)
* - 'maxDepth' => 3 最大遞歸深度(0=僅當前目錄)
* - 'followLinks' => false 是否跟隨符號鏈接
* - 'batchSize' => 1000 批次處理大小(大目錄避免記憶體峰值)
* @return array ['total' => 總數, 'byExt' => 副檔名分佈, 'dirs' => 目錄數]
*/
function countFilesAdvanced(string $dir, array $options = []): array
{
$extensions = array_map('strtolower', $options['extensions'] ?? []);
$maxDepth = $options['maxDepth'] ?? -1; // -1 無限制
$followLinks = $options['followLinks'] ?? false;
$batchSize = $options['batchSize'] ?? 1000;
$flags = FilesystemIterator::SKIP_DOTS | FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO;
if ($followLinks) $flags |= FilesystemIterator::FOLLOW_SYMLINKS;
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, $flags),
RecursiveIteratorIterator::SELF_FIRST
);
$total = 0;
$dirs = 0;
$byExt = [];
$depth = 0;
foreach ($iterator as $path => $file) {
// 深度控制
$currentDepth = $iterator->getDepth();
if ($maxDepth >= 0 && $currentDepth > $maxDepth) {
$iterator->next(); // 跳過子樹
continue;
}
if ($file->isDir()) {
$dirs++;
continue;
}
if (!$file->isFile()) continue; // 跳過 socket、fifo 等
// 副檔名過濾
if ($extensions) {
$ext = strtolower($file->getExtension());
if (!in_array($ext, $extensions, true)) continue;
$byExt[$ext] = ($byExt[$ext] ?? 0) + 1;
}
$total++;
// 批次釋放記憶體(大目錄時)
if ($total % $batchSize === 0) {
gc_collect_cycles();
}
}
return ['total' => $total, 'byExt' => $byExt, 'dirs' => $dirs];
}
// 使用範例:只統計圖片,最深 2 層
$result = countFilesAdvanced('/var/www/uploads', [
'extensions' => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
'maxDepth' => 2,
]);
echo "圖片總數: {$result['total']}\n";
echo "分佈: " . json_encode($result['byExt'], JSON_UNESCAPED_UNICODE) . "\n";
echo "子目錄數: {$result['dirs']}\n";
?>
| 方法 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
glob() | 單層、簡單模式 | 代碼極簡 | 不遞歸、大目錄記憶體峰值高、無法獲取文件元數據 |
scandir() + 遞歸函數 | 教學、極簡需求 | 無依賴 | 易漏掉 ./..、手動遞歸易出錯、效能差 |
| SPL RecursiveDirectoryIterator (推薦) | 生產環境、大目錄、需元數據 | 內建 C 層實現、記憶體友好、支援深度控制、過濾、鏈接處理、並行遍歷 | API 稍繁瑣 |
exec('find ...') | 超大目錄(百萬級)、極致效能 | 系統級優化、可用 wc -l 极快 | 依賴 Shell、跨平台差、安全風險(注入)、需解析輸出 |
is_readable() 檢查目錄權限,try-catch 捕獲 PermissionDenied(PHP 8.1+ 為 FilesystemIterator 拋出異常)。SKIP_DOTS 不跟隨鏈接,若需跟隨必須加循環檢測(記錄已訪問 inode)。SKIP_DOTS 只跳過 ./..,**不跳過** .gitignore、.env 等點文件。如需排除,手動判斷 $file->getBasename()[0] === '.'。mb_convert_encoding 或 PHP 8.2+ 的 FilesystemIterator::CURRENT_AS_SELF 獲取 SplFileInfo 後自行處理。# 僅當前目錄文件數
php -r "echo count(array_diff(scandir('.'), ['.','..']));"
# 遞歸統計(含子目錄)
php -r "echo iterator_count(new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.', FilesystemIterator::SKIP_DOTS)));"
# 統計特定副檔名
php -r "$c=0; foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.')) as $f) if($f->isFile() && in_array($f->getExtension(),['jpg','png'])) $c++; echo $c;"
統計目錄文件數看似簡單,生產環境卻暗藏權限、編碼、效能、鏈接循環等坑。**推薦以 SPL RecursiveDirectoryIterator 為核心**,封裝為可複用函數/類,支援過濾、深度控制、批次釋放記憶體。超大規模場景下,異步化 + 緩存是正解。代碼即文檔,封裝好一次,到處安心用。