PHP使用yield 读取超大型目录、大目录的方法

之前碰到一个问题,需要处理一个超大型目录,目录有多大呢,单目录有 200G 大小,大部分人的思路如下, 用日常的递归,基本上读取到的路径数组非常大,会导致超出内存,特此研究了一番:

一般常见的方法如下:

function recursiveScan($dir) {
    $files = [];
    $items = scandir($dir);

    foreach ($items as $item) {
        if ($item == '.' || $item == '..') {
            continue;
        }

        $path = $dir . '/' . $item;

        if (is_dir($path)) {
            $files = array_merge($files, recursiveScan($path)); // 递归读取子目录
        } else {
            $files[] = $path;
        }
    }

    return $files;
}

$directory = '/path/to/directory';
$files = recursiveScan($directory);

foreach ($files as $file) {
    echo $file . PHP_EOL;
}

上面的方法是很常见的做法,读取小文件夹没问题,但是读取大型乃至超大型文件夹会超出内存,通常出现如下提示:

PHP Fatal error:  Allowed memory size of 536870912 bytes exhausted (tried to allocate 134217736 bytes) 

解决思路:
1、我们将 recursiveScan 函数改为生成器函数,并使用 yield $path; 逐步生成文件路径。这样在调用这个函数时,可以逐步处理文件而不是一次性返回所有文件路径。这种方式可以减少内存占用,并在需要时逐个获取文件路径。
2、不使用递归这种耗资源的方式, 使用 array_pop 栈的思路来模拟迭代

改进过的方法如下,非常的节省内存,默认 128m 的配置即可读取超大目录:

function recursiveScan($folder) {
    $stack = [$folder]; // 使用栈来模拟迭代

    $ext_map = $this->config('ext_map');
    $ext_map_arr = explode("\n", $ext_map);

    while (!empty($stack)) {
        $currentFolder = array_pop($stack);
        $handle = opendir($currentFolder);

        while (($file = readdir($handle)) !== false) {
            if ($file != '.' && $file != '..') {
                $path = $currentFolder . '/' . $file;

                if (is_dir($path)) {
                    $stack[] = $path; //判断目录则入栈,代替迭代
                } else {

                    yield $path; // 生成器函数逐步生成文件路径
                }
            }
        }

        closedir($handle);
    }
}

使用的话,就直接用 foreach 循环上面的结果, 返回 $path 即可,跟普通的思路一样

foreach(recursiveScan($folder) as $path){
    echo $path.PHP_EOL;
}

这样就可以轻松读取 200G 甚至 2T 的文件夹了,是不是很简单了?

本作品采用《CC 协议》,转载必须注明作者和本文链接
阿修罗
本帖由系统于 11个月前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 14

看着就很厉害的样子,不过我并没有看懂,先 Mark 一下,万一日后有需求呢。

1年前 评论
summerbloom (楼主) 1年前
1年前 评论
summerbloom (楼主) 1年前

:full_moon_with_face: 其实在某种程度上,即使不存在 yeild 也可以正常运行,就比如,你把 yeild 换成一个 callback 函数,你的代码也是可以正常无压力运行的,yeild 更多的是实现了 foreach 这种形式的写法

1年前 评论
summerbloom (楼主) 1年前

读取目录扔队列里简单点吧

1年前 评论
summerbloom (楼主) 11个月前
mshx (作者) 11个月前

真实业务场景也不需要一次性读取所有目录及子目录。一般都交互操作操作到哪个目录再继续下一个了。

1年前 评论
summerbloom (楼主) 11个月前

胖友 你听说过 SPL 库嘛?

11个月前 评论
summerbloom (楼主) 11个月前
gowithwind (作者) 11个月前