PHP readdir() 函数

定义和用法

readdir() 函数用于从由 opendir() 打开的目录句柄中读取条目(文件或目录名)。

每次调用 readdir() 都会返回目录中的下一个条目,直到没有更多条目时返回 false。通常与 opendir()closedir() 函数一起使用,遍历目录内容。

注意:readdir() 返回的条目顺序依赖于文件系统,通常按照文件在目录中的存储顺序返回。如果需要按字母顺序排序,可以对结果数组进行排序。

语法

readdir(?resource $dir_handle = null): string|false

参数

参数 描述
dir_handle

可选。由 opendir() 打开的目录句柄资源。

如果省略此参数或设置为 null,函数将使用最后打开的目录句柄。

从 PHP 8.0.0 开始,此参数可以为 null。

返回值

返回值 描述
string 成功时返回目录中的一个条目名称(文件名或目录名)
FALSE 当没有更多条目可读取时返回 false

重要提示

注意:

readdir() 在遍历时会返回两个特殊的目录条目:.(当前目录)和 ..(父目录)。在大多数情况下,需要过滤掉这两个条目。

正确的使用方法是:while (false !== ($entry = readdir($handle))),而不是 while ($entry = readdir($handle)),因为如果目录中有名为 "0" 的文件,后者会提前终止循环。

示例

示例1:基本用法

<?php
// 打开目录
$dir = "/tmp";
$handle = opendir($dir);

if ($handle) {
    echo "目录内容 ($dir):<br>";

    // 正确的方式:使用 !== false 比较
    while (false !== ($entry = readdir($handle))) {
        echo htmlspecialchars($entry) . "<br>";
    }

    // 关闭目录句柄
    closedir($handle);
} else {
    echo "无法打开目录: $dir";
}
?>

示例2:过滤特殊条目并按字母排序

<?php
/**
 * 获取目录中的文件和子目录(排除 . 和 ..)
 * @param string $directory 目录路径
 * @return array 排序后的目录内容
 */
function getDirectoryContents($directory) {
    $contents = [];

    if ($handle = opendir($directory)) {
        while (false !== ($entry = readdir($handle))) {
            // 过滤 . 和 ..
            if ($entry !== '.' && $entry !== '..') {
                $contents[] = $entry;
            }
        }
        closedir($handle);

        // 按字母顺序排序
        sort($contents);
    }

    return $contents;
}

// 使用示例
$directory = "/tmp";
$contents = getDirectoryContents($directory);

echo "目录内容 ($directory) - 已排序:<br>";
if (count($contents) > 0) {
    foreach ($contents as $item) {
        echo "- " . htmlspecialchars($item) . "<br>";
    }
} else {
    echo "目录为空";
}
?>

示例3:区分文件和目录

<?php
/**
 * 分别列出目录中的文件和子目录
 * @param string $path 目录路径
 */
function listFilesAndDirs($path) {
    if (!is_dir($path)) {
        echo "$path 不是有效的目录";
        return;
    }

    $handle = opendir($path);
    if (!$handle) {
        echo "无法打开目录: $path";
        return;
    }

    $files = [];
    $directories = [];

    while (false !== ($entry = readdir($handle))) {
        if ($entry === '.' || $entry === '..') {
            continue;
        }

        $fullPath = $path . '/' . $entry;

        if (is_dir($fullPath)) {
            $directories[] = $entry;
        } elseif (is_file($fullPath)) {
            $files[] = $entry;
        }
    }

    closedir($handle);

    // 排序
    sort($files);
    sort($directories);

    // 输出结果
    echo "<h4>目录: " . htmlspecialchars($path) . "</h4>";

    echo "<h5>子目录 (" . count($directories) . "):</h5>";
    if (count($directories) > 0) {
        echo "<ul>";
        foreach ($directories as $dir) {
            echo "<li>" . htmlspecialchars($dir) . "/</li>";
        }
        echo "</ul>";
    } else {
        echo "<p>没有子目录</p>";
    }

    echo "<h5>文件 (" . count($files) . "):</h5>";
    if (count($files) > 0) {
        echo "<ul>";
        foreach ($files as $file) {
            echo "<li>" . htmlspecialchars($file) . "</li>";
        }
        echo "</ul>";
    } else {
        echo "<p>没有文件</p>";
    }
}

// 使用示例
listFilesAndDirs("/tmp");
?>

示例4:查找特定类型的文件

<?php
/**
 * 在目录中查找特定扩展名的文件
 * @param string $directory 要搜索的目录
 * @param array|string $extensions 扩展名或扩展名数组
 * @return array 匹配的文件列表
 */
function findFilesByExtension($directory, $extensions) {
    $foundFiles = [];

    // 如果传递的是字符串,转换为数组
    if (is_string($extensions)) {
        $extensions = [$extensions];
    }

    // 转换为小写以便不区分大小写比较
    $extensions = array_map('strtolower', $extensions);

    if ($handle = opendir($directory)) {
        while (false !== ($entry = readdir($handle))) {
            if ($entry === '.' || $entry === '..') {
                continue;
            }

            $fullPath = $directory . '/' . $entry;

            // 只处理文件
            if (is_file($fullPath)) {
                $fileExtension = strtolower(pathinfo($entry, PATHINFO_EXTENSION));

                if (in_array($fileExtension, $extensions)) {
                    $foundFiles[] = [
                        'name' => $entry,
                        'path' => $fullPath,
                        'size' => filesize($fullPath),
                        'modified' => filemtime($fullPath)
                    ];
                }
            }
        }
        closedir($handle);
    }

    return $foundFiles;
}

// 使用示例
$directory = "/tmp";
$phpFiles = findFilesByExtension($directory, ['php', 'php5', 'php7', 'phtml']);

echo "在 $directory 中找到的PHP文件:<br>";
if (count($phpFiles) > 0) {
    foreach ($phpFiles as $file) {
        $size = round($file['size'] / 1024, 2); // 转换为KB
        $date = date('Y-m-d H:i:s', $file['modified']);
        echo "- {$file['name']} ({$size}KB, 修改于: $date)<br>";
    }
} else {
    echo "没有找到PHP文件";
}
?>

示例5:使用 rewinddir() 重新开始读取

<?php
// 打开目录
$handle = opendir("/tmp");

if ($handle) {
    echo "第一次读取(前5个条目):<br>";
    $count = 0;
    while (false !== ($entry = readdir($handle)) && $count < 5) {
        echo $entry . "<br>";
        $count++;
    }

    echo "<br>目录指针位置: " . (int)$handle . "<br><br>";

    // 重置目录指针到开头
    rewinddir($handle);
    echo "使用 rewinddir() 重置指针后<br><br>";

    echo "第二次读取(所有条目):<br>";
    $total = 0;
    while (false !== ($entry = readdir($handle))) {
        // 跳过 . 和 ..
        if ($entry !== '.' && $entry !== '..') {
            echo $entry . "<br>";
            $total++;
        }
    }

    echo "<br>总计文件/目录数(排除 . 和 ..): $total<br>";

    closedir($handle);
}
?>

示例6:处理大型目录(分批处理)

<?php
/**
 * 分批处理大型目录,避免内存溢出
 * @param string $directory 要处理的目录
 * @param callable $processor 处理每个条目的回调函数
 * @param int $batchSize 每批处理的条目数
 * @return int 处理的条目总数
 */
function processLargeDirectory($directory, $processor, $batchSize = 100) {
    $handle = opendir($directory);
    if (!$handle) {
        return 0;
    }

    $processed = 0;
    $batchCount = 0;

    while (false !== ($entry = readdir($handle))) {
        if ($entry === '.' || $entry === '..') {
            continue;
        }

        // 处理条目
        $processor($entry, $directory);
        $processed++;
        $batchCount++;

        // 每处理 $batchSize 个条目,可以执行一些清理操作
        if ($batchCount >= $batchSize) {
            // 例如:输出进度、释放内存等
            echo "已处理 $processed 个条目...<br>";

            if (function_exists('gc_collect_cycles')) {
                gc_collect_cycles(); // 垃圾回收
            }

            $batchCount = 0;
        }
    }

    closedir($handle);

    echo "处理完成,总计 $processed 个条目<br>";
    return $processed;
}

// 使用示例:统计目录中各种文件类型的数量
$directory = "/tmp";
$fileTypes = [];

// 定义处理函数
$processor = function($entry, $dir) use (&$fileTypes) {
    $fullPath = $dir . '/' . $entry;

    if (is_file($fullPath)) {
        $extension = pathinfo($entry, PATHINFO_EXTENSION);
        if (empty($extension)) {
            $extension = '无扩展名';
        }

        if (!isset($fileTypes[$extension])) {
            $fileTypes[$extension] = 0;
        }
        $fileTypes[$extension]++;
    }
};

echo "开始处理目录: $directory<br>";
processLargeDirectory($directory, $processor, 50);

echo "<br>文件类型统计:<br>";
if (count($fileTypes) > 0) {
    arsort($fileTypes); // 按数量降序排序
    foreach ($fileTypes as $type => $count) {
        echo "$type: $count 个文件<br>";
    }
}
?>

readdir() 循环的常见错误模式

错误写法 问题 正确写法
while ($file = readdir($handle)) 如果目录中有名为 "0"、"false" 或空字符串的文件,循环会提前终止 while (false !== ($file = readdir($handle)))
while (($file = readdir($handle)) != false) != 操作符会进行类型转换,"0" 会被转换为 false while (false !== ($file = readdir($handle)))
忘记过滤 . 和 .. 会包含当前目录和父目录的引用,通常这不是期望的结果 在循环内添加条件:if ($file == '.' || $file == '..') continue;
忘记关闭目录句柄 可能导致资源泄漏,特别是在长时间运行的脚本中 始终在最后调用 closedir($handle)

性能比较:readdir() vs scandir()

特性 readdir() scandir()
内存使用 低(一次读取一个条目) 高(一次性加载所有条目到数组)
性能 适合大型目录,可提前中断 适合小型目录,简单直接
控制能力 高(可以控制读取过程) 低(必须处理整个数组)
资源管理 需要手动打开和关闭句柄 自动管理,不需要显式关闭
排序 需要手动排序结果 可指定排序方式
推荐场景 大型目录、需要流式处理、提前中断 小型目录、需要简单列表、需要排序

最佳实践

  1. 使用严格的比较:始终使用 !== false 而不是 != false 或简单的赋值
  2. 过滤特殊条目:总是过滤掉 ... 条目,除非确实需要它们
  3. 资源清理:使用 try-finally 或确保在所有代码路径上都调用 closedir()
  4. 错误处理:检查 opendir() 的返回值,并适当处理错误
  5. 路径安全:验证用户输入的目录路径,防止目录遍历攻击
  6. 编码处理:使用 htmlspecialchars() 输出文件名,防止 XSS 攻击
  7. 性能考虑:对于大型目录,考虑分批处理或使用专门的目录迭代器

相关函数