首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 遞歸掃描目錄統計文件大小與數量完整實現

     編輯:PHP 相關知識

PHP 遞歸掃描目錄統計文件大小與數量完整實現

使用 PHP 內置目錄函數配合遞歸算法,可實現完整的目錄樹掃描、文件大小統計、人類可讀格式化輸出。本文提供生產級代碼與效能優化建議。

一、核心實現:遞歸掃描與大小統計

<?php
declare(strict_types=1);

class DirectoryScanner {
    private array $stats = [
        'files' => 0,
        'dirs' => 0,
        'total_size' => 0,
        'errors' => [],
    ];
    
    private array $options = [
        'follow_links' => false,      // 是否跟隨軟鏈接
        'max_depth' => -1,            // 最大遞歸深度(-1 無限制)
        'skip_hidden' => true,        // 跳過隱藏文件/目錄
        'extensions' => [],           // 只統計指定擴展名(空=全部)
        'exclude_patterns' => [],     // 排除正則模式
    ];

    public function __construct(array $options = []) {
        $this->options = array_merge($this->options, $options);
    }

    // 公開入口
    public function scan(string $path): array {
        if (!is_dir($path) || !is_readable($path)) {
            throw new \InvalidArgumentException("路徑不存在或不可讀: {$path}");
        }
        
        $this->stats = ['files' => 0, 'dirs' => 0, 'total_size' => 0, 'errors' => []];
        $this->scanDirectory($path, 0);
        
        return [
            'path' => realpath($path),
            'files' => $this->stats['files'],
            'dirs' => $this->stats['dirs'],
            'total_size' => $this->stats['total_size'],
            'total_size_human' => $this->formatBytes($this->stats['total_size']),
            'errors' => $this->stats['errors'],
        ];
    }

    // 遞歸核心
    private function scanDirectory(string $path, int $depth): void {
        // 深度限制
        if ($this->options['max_depth'] > 0 && $depth >= $this->options['max_depth']) {
            return;
        }

        $handle = @opendir($path);
        if (!$handle) {
            $this->stats['errors'][] = "無法打開目錄: {$path}";
            return;
        }

        while (($entry = readdir($handle)) !== false) {
            // 跳過 . 和 ..
            if ($entry === '.' || $entry === '..') continue;
            
            // 跳過隱藏文件
            if ($this->options['skip_hidden'] && str_starts_with($entry, '.')) continue;
            
            $fullPath = $path . DIRECTORY_SEPARATOR . $entry;
            
            // 排除模式匹配
            if ($this->matchesExclude($fullPath)) continue;

            try {
                if (is_link($fullPath)) {
                    // 軟鏈接處理
                    if ($this->options['follow_links'] && is_dir($fullPath)) {
                        $this->stats['dirs']++;
                        $this->scanDirectory($fullPath, $depth + 1);
                    }
                    // 文件鏈接按文件處理
                    elseif (is_file($fullPath)) {
                        $this->processFile($fullPath);
                    }
                }
                elseif (is_dir($fullPath)) {
                    $this->stats['dirs']++;
                    $this->scanDirectory($fullPath, $depth + 1);
                }
                elseif (is_file($fullPath)) {
                    $this->processFile($fullPath);
                }
            } catch (\Throwable $e) {
                $this->stats['errors'][] = "處理失敗 {$fullPath}: " . $e->getMessage();
            }
        }
        closedir($handle);
    }

    private function processFile(string $path): void {
        // 擴展名過濾
        if ($this->options['extensions']) {
            $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
            if (!in_array($ext, $this->options['extensions'], true)) return;
        }
        
        $size = @filesize($path);
        if ($size !== false) {
            $this->stats['files']++;
            $this->stats['total_size'] += $size;
        }
    }

    private function matchesExclude(string $path): bool {
        foreach ($this->options['exclude_patterns'] as $pattern) {
            if (preg_match($pattern, $path)) return true;
        }
        return false;
    }

    // 格式化位元組
    public function formatBytes(int $bytes, int $precision = 2): string {
        $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);
        $value = $bytes / (1024 ** $pow);
        return round($value, $precision) . ' ' . $units[$pow];
    }
}

二、使用範例

<?php
require_once 'DirectoryScanner.php';

$scanner = new DirectoryScanner([
    'follow_links' => false,
    'max_depth' => 10,
    'skip_hidden' => true,
    'extensions' => ['php', 'js', 'css', 'html', 'vue', 'md'], // 只統計代碼文件
    'exclude_patterns' => [
        '/vendor/',           // 排除 composer 依賴
        '/node_modules/',     // 排除 npm 依賴
        '/\.git/',            // 排除 git 目錄
        '/storage\/logs/',    // 排除日誌
        '/\.cache$/',         // 排除緩存文件
    ],
]);

try {
    $result = $scanner->scan('/var/www/myproject');
    
    echo "掃描路徑: {$result['path']}\n";
    echo "文件數量: {$result['files']}\n";
    echo "目錄數量: {$result['dirs']}\n";
    echo "總大小: {$result['total_size_human']} ({$result['total_size']} bytes)\n";
    
    if ($result['errors']) {
        echo "\n錯誤:\n";
        foreach ($result['errors'] as $err) echo "  - {$err}\n";
    }
} catch (\Throwable $e) {
    echo "掃描失敗: " . $e->getMessage();
}

三、進階:生成詳細文件列表報表

<?php
class DetailedScanner extends DirectoryScanner {
    private array $fileList = [];
    
    protected function processFile(string $path): void {
        parent::processFile($path);
        
        $stat = stat($path);
        $this->fileList[] = [
            'path' => $path,
            'relative_path' => $this->getRelativePath($path),
            'size' => $stat['size'],
            'size_human' => $this->formatBytes($stat['size']),
            'modified' => date('Y-m-d H:i:s', $stat['mtime']),
            'extension' => strtolower(pathinfo($path, PATHINFO_EXTENSION)),
            'permissions' => substr(sprintf('%o', $stat['mode']), -4),
        ];
    }
    
    public function getFileList(): array {
        return $this->fileList;
    }
    
    public function exportCsv(string $filename): void {
        $fp = fopen($filename, 'w');
        fputcsv($fp, ['路徑', '大小', '修改時間', '擴展名', '權限']);
        foreach ($this->fileList as $file) {
            fputcsv($fp, [
                $file['relative_path'],
                $file['size_human'],
                $file['modified'],
                $file['extension'],
                $file['permissions'],
            ]);
        }
        fclose($fp);
    }
    
    private function getRelativePath(string $path): string {
        // 實現相對路徑邏輯
        return $path;
    }
}

// 使用
$scanner = new DetailedScanner(['extensions' => ['log']]);
$scanner->scan('/var/log/nginx');
$scanner->exportCsv('nginx_logs_report.csv');

四、非遞歸替代方案(大深度目錄避免棧溢出)

<?php
function scanDirectoryIterative(string $rootPath): array {
    $stats = ['files' => 0, 'dirs' => 0, 'total_size' => 0];
    $stack = [$rootPath];
    
    while ($stack) {
        $currentDir = array_pop($stack);
        $handle = @opendir($currentDir);
        if (!$handle) continue;
        
        while (($entry = readdir($handle)) !== false) {
            if ($entry === '.' || $entry === '..') continue;
            $fullPath = $currentDir . DIRECTORY_SEPARATOR . $entry;
            
            if (is_dir($fullPath) && !is_link($fullPath)) {
                $stats['dirs']++;
                $stack[] = $fullPath;
            } elseif (is_file($fullPath)) {
                $size = @filesize($fullPath);
                if ($size !== false) {
                    $stats['files']++;
                    $stats['total_size'] += $size;
                }
            }
        }
        closedir($handle);
    }
    return $stats;
}

五、SPL 迭代器現代寫法(PHP 5.3+)

<?php
function scanWithSpl(string $path, array $extensions = []): array {
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );
    
    $stats = ['files' => 0, 'dirs' => 0, 'total_size' => 0, 'files_list' => []];
    
    foreach ($iterator as $fileInfo) {
        if ($fileInfo->isDir()) {
            $stats['dirs']++;
        } elseif ($fileInfo->isFile()) {
            $ext = strtolower($fileInfo->getExtension());
            if ($extensions && !in_array($ext, $extensions, true)) continue;
            
            $size = $fileInfo->getSize();
            $stats['files']++;
            $stats['total_size'] += $size;
            $stats['files_list'][] = [
                'path' => $fileInfo->getPathname(),
                'size' => $size,
                'modified' => $fileInfo->getMTime(),
            ];
        }
    }
    return $stats;
}

六、性能對比與選擇建議

方案優點缺點適用場景
遞歸函數代碼簡潔、易理解、易擴展極深目錄可能棧溢出常規項目、深度 < 1000
手動棧迭代無深度限制、內存可控代碼較複雜超深目錄、大規模掃描
SPL RecursiveIterator內置、標準化、支援過濾器性能略低、自定義邏輯繁瑣快速原型、標準化需求
exec('du -sh')系統級、極快、含磁盤佔用依賴 Linux、需權限、無法過濾服務器盤面統計、CI/CD

七、生產環境注意事項

  1. 權限問題:掃描前檢查 is_readable(),捕獲異常記錄而非中斷
  2. 超大目錄:設置 max_depth、exclude_patterns 避免掃描 vendor/node_modules
  3. 執行時間:CLI 執行 set_time_limit(0);Web 環境改用隊列異步處理
  4. 內存控制:避免一次性載入所有文件信息,大報表用生成器/分批寫入 CSV
  5. 符號鏈接:小心循環鏈接導致無限遞歸,默認 follow_links = false

根據項目規模與環境選擇合適方案,配合排除規則與異步處理,能高效完成目錄統計任務。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved