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

PHP 目錄遞迴遍歷:從簡易函式到 SPL 迭代器、過濾器、非同步處理的現代寫法

     編輯:PHP 綜合知識

前言

目錄遍歷是檔案處理的基礎操作:掃描上傳目錄、建立檔案索引、清理暫存檔、生成網站地圖。早期寫法使用 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');
?>

缺點:遞迴深度受限於堆疊、一次載入所有路徑佔記憶體、無法中斷、難以過濾/排序。

SPL 迭代器現代寫法

1. RecursiveDirectoryIterator + RecursiveIteratorIterator

<?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()
}
?>

2. 記憶體高效:生成器包裝

<?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";
    }
}
?>

3. 進階過濾鏈

<?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;
    }
}
?>

使用範例

1. 尋找大於 10MB 的圖片檔案

<?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";
}
?>

2. 清理 30 天前的暫存檔

<?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";
?>

3. 產生網站地圖 XML

<?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~4MBCPU 密集型加速、需擴充
rsync/find 系統呼叫~0.15s~0.5MB最快、但依賴外部指令、難以整合邏輯

結語

目錄遍歷從簡易遞迴邁向 SPL 迭代器、生成器、併行處理,是 PHP 現代化的縮影。掌握 RecursiveDirectoryIterator、CallbackFilterIterator、Generator 組合技巧,能以極低記憶體、高擴充性解決各類檔案掃描需求。生產環境建議封裝為 DirectoryScanner 服務類,統一過濾邏輯、進度回呼、錯誤處理,並提供統計、併行等進階功能。

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