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

PHP 目錄遞迴掃描類:檔案與資料夾列表完整解決方案

     編輯:PHP 綜合知識

PHP 目錄掃描工具類完整實作

專案常需掃描目錄結構:建立檔案索引、產生網站地圖、批次處理檔案等。本文提供功能完整的 `DirectoryScanner` 類別,支援資料夾列表、檔案列表、樹狀結構三種模式,含錯誤處理、效能優化與現代 PHP 寫法。

完整類別實作

<?php
/**
 * 目錄掃描器
 * 支援: 資料夾列表、檔案列表、樹狀結構
 * 
 * 使用範例:
 * $scanner = new DirectoryScanner();
 * $dirs = $scanner->getDirectories('/path/to/dir');
 * $files = $scanner->getFiles('/path/to/dir');
 * $tree = $scanner->getTree('/path/to/dir');
 */
class DirectoryScanner {
    // 掃描選項常數
    public const SKIP_DOTS = 1;           // 跳過 . 和 ..
    public const SKIP_HIDDEN = 2;         // 跳過隱藏檔 (以 . 開頭)
    public const FULL_PATH = 4;           // 返回完整路徑
    public const RECURSIVE = 8;           // 遞迴掃描 (僅 getTree)
    public const SORT_BY_NAME = 16;       // 按名稱排序
    public const SORT_BY_TIME = 32;       // 按修改時間排序
    public const SORT_DESC = 64;          // 降序
    public const ONLY_FILES = 128;        // 僅檔案
    public const ONLY_DIRS = 256;         // 僅資料夾
    
    private $defaultOptions;
    
    public function __construct(int $options = 0) {
        $this->defaultOptions = $options ?: (self::SKIP_DOTS | self::SORT_BY_NAME);
    }
    
    /**
     * 取得資料夾列表 (單層)
     */
    public function getDirectories(string $path, int $options = 0): array {
        return $this->scan($path, $options | self::ONLY_DIRS);
    }
    
    /**
     * 取得檔案列表 (單層)
     */
    public function getFiles(string $path, int $options = 0): array {
        return $this->scan($path, $options | self::ONLY_FILES);
    }
    
    /**
     * 取得樹狀結構 (遞迴)
     */
    public function getTree(string $path, int $options = 0): array {
        $options |= self::RECURSIVE | self::FULL_PATH;
        return $this->scan($path, $options);
    }
    
    /**
     * 核心掃描邏輯
     */
    private function scan(string $path, int $options): array {
        $path = rtrim($path, DIRECTORY_SEPARATOR);
        
        if (!is_dir($path)) {
            throw new InvalidArgumentException("路徑不存在或非目錄: {$path}");
        }
        
        if (!is_readable($path)) {
            throw new RuntimeException("目錄不可讀: {$path}");
        }
        
        $results = [];
        $handle = @opendir($path);
        
        if ($handle === false) {
            throw new RuntimeException("無法開啟目錄: {$path}");
        }
        
        try {
            while (($entry = readdir($handle)) !== false) {
                // 過濾 . 和 ..
                if (($options & self::SKIP_DOTS) && ($entry === '.' || $entry === '..')) {
                    continue;
                }
                
                // 過濾隱藏檔
                if (($options & self::SKIP_HIDDEN) && $entry[0] === '.') {
                    continue;
                }
                
                $fullPath = $path . DIRECTORY_SEPARATOR . $entry;
                $isDir = is_dir($fullPath);
                
                // 類型過濾
                if (($options & self::ONLY_FILES) && $isDir) continue;
                if (($options & self::ONLY_DIRS) && !$isDir) continue;
                
                $item = ($options & self::FULL_PATH) ? $fullPath : $entry;
                
                // 遞迴掃描子目錄 (僅 getTree)
                if ($isDir && ($options & self::RECURSIVE)) {
                    $item = [
                        'name' => $entry,
                        'path' => $fullPath,
                        'type' => 'directory',
                        'children' => $this->scan($fullPath, $options)
                    ];
                } elseif (!$isDir) {
                    // 檔案可選擇附加中繼資料
                    if ($options & self::FULL_PATH) {
                        $item = [
                            'name' => $entry,
                            'path' => $fullPath,
                            'type' => 'file',
                            'size' => filesize($fullPath),
                            'mtime' => filemtime($fullPath),
                            'ext' => pathinfo($entry, PATHINFO_EXTENSION)
                        ];
                    }
                }
                
                $results[] = $item;
            }
        } finally {
            closedir($handle);
        }
        
        // 排序
        if ($options & (self::SORT_BY_NAME | self::SORT_BY_TIME)) {
            $this->sortResults($results, $options);
        }
        
        return $results;
    }
    
    private function sortResults(array &$results, int $options): void {
        $desc = ($options & self::SORT_DESC) !== 0;
        $byTime = ($options & self::SORT_BY_TIME) !== 0;
        
        usort($results, function($a, $b) use ($byTime, $desc, $options) {
            // 處理樹狀結構 (陣列) vs 簡單字串
            $nameA = is_array($a) ? $a['name'] : $a;
            $nameB = is_array($b) ? $b['name'] : $b;
            
            $timeA = is_array($a) && isset($a['mtime']) ? $a['mtime'] : 
                     (is_array($a) ? filemtime($a['path']) : filemtime($a));
            $timeB = is_array($b) && isset($b['mtime']) ? $b['mtime'] : 
                     (is_array($b) ? filemtime($b['path']) : filemtime($b));
            
            if ($byTime) {
                $cmp = $timeA <=> $timeB;
            } else {
                $cmp = strnatcasecmp($nameA, $nameB);  // 自然排序
            }
            
            return $desc ? -$cmp : $cmp;
        });
    }
    
    // 靜態快捷方法
    public static function dirs(string $path, int $options = 0): array {
        return (new self())->getDirectories($path, $options);
    }
    
    public static function files(string $path, int $options = 0): array {
        return (new self())->getFiles($path, $options);
    }
    
    public static function tree(string $path, int $options = 0): array {
        return (new self())->getTree($path, $options);
    }
}
?>

使用範例

<?php
require_once 'DirectoryScanner.php';

// 1. 簡單資料夾列表
$dirs = DirectoryScanner::dirs('./example');
print_r($dirs);
/*
Array
(
    [0] => example_one
    [1] => example_two
)
*/

// 2. 簡單檔案列表
$files = DirectoryScanner::files('./example/example_one');
print_r($files);
/*
Array
(
    [0] => example.sql
    [1] => example.txt
)
*/

// 3. 完整樹狀結構
$tree = DirectoryScanner::tree('./example', 
    DirectoryScanner::FULL_PATH | 
    DirectoryScanner::SORT_BY_TIME | 
    DirectoryScanner::SORT_DESC
);
print_r($tree);
/*
Array
(
    [0] => Array
        (
            [name] => example_one
            [path] => ./example/example_one
            [type] => directory
            [children] => Array
                (
                    [0] => Array
                        (
                            [name] => example.sql
                            [path] => ./example/example_one/example.sql
                            [type] => file
                            [size] => 1024
                            [mtime] => 1640000000
                            [ext] => sql
                        )
                    [1] => ...
                )
        )
    [1] => ...
)
*/

// 4. 進階:僅掃描 PHP 檔案,遞迴,按時間倒序
$phpFiles = [];
$scanner = new DirectoryScanner(DirectoryScanner::RECURSIVE | DirectoryScanner::SORT_BY_TIME | DirectoryScanner::SORT_DESC);
$all = $scanner->getTree('./src');

function collectPhpFiles(array $tree, array &$result): void {
    foreach ($tree as $node) {
        if ($node['type'] === 'file' && $node['ext'] === 'php') {
            $result[] = $node;
        } elseif ($node['type'] === 'directory') {
            collectPhpFiles($node['children'], $result);
        }
    }
}

collectPhpFiles($all, $phpFiles);
print_r($phpFiles);
?>

現代替代方案:SPL 迭代器

PHP 5.3+ 內建 RecursiveDirectoryIterator,無需自建類別:

<?php
// 簡單遞迴列表
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator('./example', RecursiveDirectoryIterator::SKIP_DOTS),
    RecursiveIteratorIterator::SELF_FIRST
);

foreach ($iterator as $path => $fileInfo) {
    $prefix = $fileInfo->isDir() ? '[D] ' : '[F] ';
    echo $prefix . $fileInfo->getPathname() . "\n";
}

// 過濾 PHP 檔案
$filter = new RegexIterator(
    new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator('./src', RecursiveDirectoryIterator::SKIP_DOTS)
    ),
    '/\.php$/i'
);

foreach ($filter as $file) {
    echo $file->getPathname() . "\n";
}

// 取得檔案資訊陣列
$files = iterator_to_array(
    new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator('./example', RecursiveDirectoryIterator::SKIP_DOTS)
    ),
    false
);

// 只取檔案 (非目錄)
$filesOnly = array_filter($files, fn($f) => $f->isFile());
?>

效能比較與選擇建議

方案優點缺點適用場景
自建類別 (opendir/readdir)完全可控、輕量、無依賴、易擴充中繼資料需自行處理遞迴、排序、錯誤特定格式輸出、效能敏感、舊 PHP 版本
SPL RecursiveDirectoryIterator標準庫、物件導向、支援過濾器鏈、功能豐富物件開銷較大、深度遞迴可能較慢現代專案、複雜過濾、標準化需求
glob()簡單、支援模式匹配非遞迴 (需 GLOB_BRACE)、記憶體載入所有結果單層模式匹配、小目錄
scandir()簡單、返回陣列非遞迴、含 . 和 ..、無過濾簡單單層列表

大型目錄掃描優化技巧

  • 生成器模式:使用 `yield` 逐個產出,避免記憶體爆炸
  • 非同步掃描:Swoole/Workerman 環境下使用協程並行
  • 快取結果:目錄結構變動頻率低時快取掃描結果
  • 增量掃描:記錄上次掃描時間,僅處理變更檔案 (inotify/FSEvents)
  • 限制深度:避免無限遞迴 (符號連結循環)
<?php
// 生成器版本:記憶體友善
function scanDirectoryGenerator(string $path, int $maxDepth = -1, int $currentDepth = 0): Generator {
    if ($maxDepth >= 0 && $currentDepth > $maxDepth) return;
    
    $handle = @opendir($path);
    if (!$handle) return;
    
    try {
        while (($entry = readdir($handle)) !== false) {
            if ($entry === '.' || $entry === '..') continue;
            
            $fullPath = $path . DIRECTORY_SEPARATOR . $entry;
            $isDir = is_dir($fullPath);
            
            yield [
                'name' => $entry,
                'path' => $fullPath,
                'type' => $isDir ? 'directory' : 'file',
                'depth' => $currentDepth
            ];
            
            if ($isDir) {
                yield from scanDirectoryGenerator($fullPath, $maxDepth, $currentDepth + 1);
            }
        }
    } finally {
        closedir($handle);
    }
}

// 使用:逐個處理,不佔用大量記憶體
foreach (scanDirectoryGenerator('./huge_directory', 3) as $item) {
    // 處理單項
    if ($item['type'] === 'file' && $item['depth'] <= 2) {
        // ...
    }
}
?>

根據專案規模、PHP 版本、功能需求選擇適當方案。現代專案優先考慮 SPL 迭代器或生成器模式,兼顧標準化與效能。

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