PHP scandir() 函数

定义和用法

scandir() 函数返回指定目录中的文件和目录的数组。该函数提供了一种简单的方式来获取目录内容,无需手动打开和读取目录句柄。

opendir()/readdir()/closedir() 相比,scandir() 更简单易用,因为它一次性返回所有结果,并且不需要显式的资源管理。

注意:scandir() 默认返回一个包含所有文件和目录的数组,包括特殊条目 .(当前目录)和 ..(父目录)。通常需要过滤掉这些条目。

语法

scandir(string $directory, int $sorting_order = SCANDIR_SORT_ASCENDING, ?resource $context = null): array|false

参数

参数 描述
directory

必需。要扫描的目录路径。

可以是绝对路径或相对路径。相对路径将相对于当前工作目录进行解析。

sorting_order

可选。指定排序方式:

  • SCANDIR_SORT_ASCENDING(默认)- 按字母升序排列
  • SCANDIR_SORT_DESCENDING - 按字母降序排列
  • SCANDIR_SORT_NONE - 不排序,按系统返回顺序

使用 SCANDIR_SORT_NONE 可以提高性能,特别是处理大型目录时。

context

可选。流上下文(stream context)资源。

用于指定流的特殊选项,例如访问 FTP、HTTP 或其他流包装器时需要。

返回值

返回值 描述
array 成功时返回包含目录中所有文件和目录的数组
FALSE 扫描失败时返回 false(目录不存在、没有权限或路径无效)

示例

示例1:基本用法

<?php
// 获取目录内容
$directory = "/tmp";
$files = scandir($directory);

if ($files !== false) {
    echo "目录 $directory 中的内容:<br>";
    foreach ($files as $file) {
        echo htmlspecialchars($file) . "<br>";
    }

    echo "<br>总计: " . count($files) . " 个条目";
} else {
    echo "无法扫描目录: $directory";
}
?>

示例2:使用不同的排序方式

<?php
$directory = "/tmp";

// 默认升序排序
$ascending = scandir($directory, SCANDIR_SORT_ASCENDING);
echo "升序排序:<br>";
print_r($ascending);
echo "<br><br>";

// 降序排序
$descending = scandir($directory, SCANDIR_SORT_DESCENDING);
echo "降序排序:<br>";
print_r($descending);
echo "<br><br>";

// 不排序(系统顺序)
$none = scandir($directory, SCANDIR_SORT_NONE);
echo "不排序(系统顺序):<br>";
print_r($none);

// 性能对比
echo "<br><br>性能统计:<br>";
$start = microtime(true);
scandir($directory, SCANDIR_SORT_ASCENDING);
$time_asc = microtime(true) - $start;

$start = microtime(true);
scandir($directory, SCANDIR_SORT_NONE);
$time_none = microtime(true) - $start;

echo "升序排序耗时: " . round($time_asc * 1000, 3) . " 毫秒<br>";
echo "不排序耗时: " . round($time_none * 1000, 3) . " 毫秒<br>";
echo "性能提升: " . round((1 - $time_none/$time_asc) * 100, 2) . "%";
?>

示例3:过滤特殊条目和特定类型的文件

<?php
/**
 * 获取目录中所有图片文件
 * @param string $directory 目录路径
 * @return array 图片文件数组
 */
function getImageFiles($directory) {
    $allFiles = scandir($directory);
    if ($allFiles === false) {
        return [];
    }

    $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
    $imageFiles = [];

    foreach ($allFiles as $file) {
        // 跳过 . 和 ..
        if ($file === '.' || $file === '..') {
            continue;
        }

        // 检查是否是文件
        $fullPath = $directory . '/' . $file;
        if (!is_file($fullPath)) {
            continue;
        }

        // 检查扩展名
        $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
        if (in_array($extension, $imageExtensions)) {
            $imageFiles[] = $file;
        }
    }

    return $imageFiles;
}

/**
 * 获取目录中所有子目录
 * @param string $directory 目录路径
 * @return array 子目录数组
 */
function getSubdirectories($directory) {
    $allItems = scandir($directory);
    if ($allItems === false) {
        return [];
    }

    $subdirectories = [];

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

        $fullPath = $directory . '/' . $item;
        if (is_dir($fullPath)) {
            $subdirectories[] = $item;
        }
    }

    return $subdirectories;
}

// 使用示例
$directory = "/var/www/html";
echo "目录: $directory<br><br>";

$images = getImageFiles($directory);
echo "图片文件 (" . count($images) . "):<br>";
foreach ($images as $image) {
    echo "- $image<br>";
}

echo "<br>";

$subdirs = getSubdirectories($directory);
echo "子目录 (" . count($subdirs) . "):<br>";
foreach ($subdirs as $dir) {
    echo "- $dir/<br>";
}
?>

示例4:递归扫描目录

<?php
/**
 * 递归扫描目录,返回所有文件和目录的列表
 * @param string $directory 起始目录
 * @param bool $includeDirs 是否在结果中包含目录
 * @return array 所有文件和目录的数组
 */
function scanDirectoryRecursive($directory, $includeDirs = true) {
    $result = [];

    // 扫描当前目录
    $items = scandir($directory);
    if ($items === false) {
        return $result;
    }

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

        $fullPath = $directory . DIRECTORY_SEPARATOR . $item;

        if (is_dir($fullPath)) {
            // 如果是目录,递归扫描
            if ($includeDirs) {
                $result[] = $fullPath;
            }
            $result = array_merge($result, scanDirectoryRecursive($fullPath, $includeDirs));
        } else {
            // 如果是文件,添加到结果
            $result[] = $fullPath;
        }
    }

    return $result;
}

/**
 * 递归扫描目录,按类型分类
 * @param string $directory 起始目录
 * @return array 分类后的结果
 */
function scanDirectoryByType($directory) {
    $result = [
        'files' => [],
        'directories' => []
    ];

    $items = scandir($directory);
    if ($items === false) {
        return $result;
    }

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

        $fullPath = $directory . DIRECTORY_SEPARATOR . $item;

        if (is_dir($fullPath)) {
            $result['directories'][] = $fullPath;
            // 递归扫描子目录
            $subResult = scanDirectoryByType($fullPath);
            $result['files'] = array_merge($result['files'], $subResult['files']);
            $result['directories'] = array_merge($result['directories'], $subResult['directories']);
        } else {
            $result['files'][] = $fullPath;
        }
    }

    return $result;
}

// 使用示例
$startDir = "/var/www";
echo "递归扫描目录: $startDir<br><br>";

// 方法1:所有文件和目录
$allItems = scanDirectoryRecursive($startDir);
echo "所有项目 (" . count($allItems) . "):<br>";
foreach (array_slice($allItems, 0, 10) as $item) { // 只显示前10个
    echo "- $item<br>";
}
if (count($allItems) > 10) {
    echo "... 还有 " . (count($allItems) - 10) . " 个项目<br>";
}

echo "<br>";

// 方法2:按类型分类
$categorized = scanDirectoryByType($startDir);
echo "文件总数: " . count($categorized['files']) . "<br>";
echo "目录总数: " . count($categorized['directories']) . "<br>";
?>

示例5:使用流上下文

<?php
// 访问FTP服务器的示例
$ftpUrl = "ftp://example.com/public_html/";

// 创建FTP上下文选项
$options = [
    'ftp' => [
        'username' => 'ftp_user',
        'password' => 'ftp_password',
        'timeout' => 30
    ]
];

// 创建流上下文
$context = stream_context_create($options);

// 使用上下文扫描FTP目录
try {
    $files = @scandir($ftpUrl, SCANDIR_SORT_ASCENDING, $context);

    if ($files !== false) {
        echo "FTP目录内容:<br>";
        foreach ($files as $file) {
            echo htmlspecialchars($file) . "<br>";
        }
    } else {
        echo "无法访问FTP目录,请检查连接参数";
    }
} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
}

// 另一个示例:使用自定义HTTP头访问Web目录(如果支持)
$httpUrl = "http://example.com/directory/";
$httpOptions = [
    'http' => [
        'method' => 'GET',
        'header' => "User-Agent: Mozilla/5.0\r\n"
    ]
];

$httpContext = stream_context_create($httpOptions);
// 注意:大多数HTTP服务器不返回目录列表,这里只是示例
?>

示例6:实用工具 - 目录统计和分析

<?php
/**
 * 获取目录的详细统计信息
 * @param string $directory 目录路径
 * @return array 统计信息
 */
function getDirectoryStats($directory) {
    $stats = [
        'total_files' => 0,
        'total_dirs' => 0,
        'total_size' => 0,
        'file_types' => [],
        'largest_file' => ['name' => '', 'size' => 0],
        'newest_file' => ['name' => '', 'time' => 0],
        'oldest_file' => ['name' => '', 'time' => PHP_INT_MAX]
    ];

    $items = scandir($directory);
    if ($items === false) {
        return $stats;
    }

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

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

        if (is_dir($fullPath)) {
            $stats['total_dirs']++;

            // 递归统计子目录
            $subStats = getDirectoryStats($fullPath);
            $stats['total_files'] += $subStats['total_files'];
            $stats['total_dirs'] += $subStats['total_dirs'];
            $stats['total_size'] += $subStats['total_size'];

            // 合并文件类型统计
            foreach ($subStats['file_types'] as $type => $count) {
                if (!isset($stats['file_types'][$type])) {
                    $stats['file_types'][$type] = 0;
                }
                $stats['file_types'][$type] += $count;
            }

            // 更新最大文件等信息
            if ($subStats['largest_file']['size'] > $stats['largest_file']['size']) {
                $stats['largest_file'] = $subStats['largest_file'];
            }

        } elseif (is_file($fullPath)) {
            $stats['total_files']++;
            $fileSize = filesize($fullPath);
            $stats['total_size'] += $fileSize;

            // 文件类型统计
            $extension = strtolower(pathinfo($item, PATHINFO_EXTENSION));
            if (empty($extension)) {
                $extension = '无扩展名';
            }

            if (!isset($stats['file_types'][$extension])) {
                $stats['file_types'][$extension] = 0;
            }
            $stats['file_types'][$extension]++;

            // 更新最大文件
            if ($fileSize > $stats['largest_file']['size']) {
                $stats['largest_file'] = ['name' => $fullPath, 'size' => $fileSize];
            }

            // 更新最新/最旧文件
            $modTime = filemtime($fullPath);
            if ($modTime > $stats['newest_file']['time']) {
                $stats['newest_file'] = ['name' => $fullPath, 'time' => $modTime];
            }
            if ($modTime < $stats['oldest_file']['time']) {
                $stats['oldest_file'] = ['name' => $fullPath, 'time' => $modTime];
            }
        }
    }

    return $stats;
}

/**
 * 格式化字节大小
 * @param int $bytes 字节数
 * @param int $precision 小数位数
 * @return string 格式化后的字符串
 */
function formatBytes($bytes, $precision = 2) {
    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
    $bytes = max($bytes, 0);
    $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
    $pow = min($pow, count($units) - 1);
    $bytes /= pow(1024, $pow);
    return round($bytes, $precision) . ' ' . $units[$pow];
}

// 使用示例
$directory = "/var/www";
$stats = getDirectoryStats($directory);

echo "<h4>目录统计: $directory</h4>";
echo "文件总数: " . number_format($stats['total_files']) . "<br>";
echo "目录总数: " . number_format($stats['total_dirs']) . "<br>";
echo "总大小: " . formatBytes($stats['total_size']) . "<br><br>";

echo "最大文件: " . basename($stats['largest_file']['name']) .
     " (" . formatBytes($stats['largest_file']['size']) . ")<br>";
echo "最新文件: " . basename($stats['newest_file']['name']) .
     " (" . date('Y-m-d H:i:s', $stats['newest_file']['time']) . ")<br>";
echo "最旧文件: " . basename($stats['oldest_file']['name']) .
     " (" . date('Y-m-d H:i:s', $stats['oldest_file']['time']) . ")<br><br>";

echo "文件类型分布:<br>";
arsort($stats['file_types']); // 按数量降序排序
foreach ($stats['file_types'] as $type => $count) {
    $percentage = round(($count / $stats['total_files']) * 100, 2);
    echo "$type: $count 个文件 ($percentage%)<br>";
}
?>

scandir() 与 opendir()/readdir() 的比较

特性 scandir() opendir()/readdir()
易用性 简单,一次调用返回所有结果 复杂,需要多个函数配合
内存使用 高(一次性加载所有条目) 低(一次读取一个条目)
性能 适合小型目录 适合大型目录,可提前中断
控制能力 低(必须处理整个数组) 高(可以控制读取过程)
资源管理 自动管理,无显式关闭 需要手动打开和关闭
排序选项 内置排序功能 需要手动排序
推荐场景 小型目录、简单列表、需要排序 大型目录、流式处理、需要提前中断

scandir() 与 glob() 的比较

特性 scandir() glob()
返回内容 所有文件和目录 匹配模式的文件/目录
过滤能力 无内置过滤,需手动过滤 支持通配符模式过滤
排序选项 支持升序、降序、不排序 返回结果已排序
递归支持 不支持递归(需手动实现) 不支持递归(但有递归扩展)
性能 通常比glob()稍快 模式匹配有额外开销
使用场景 需要所有条目、需要排序控制 需要模式匹配、通配符过滤

最佳实践

  1. 始终检查返回值:scandir() 可能返回 false,调用前应检查
  2. 过滤特殊条目:通常需要过滤 ... 这两个特殊目录
  3. 处理大型目录:对于大型目录,考虑使用 SCANDIR_SORT_NONE 提高性能,或使用 opendir()/readdir()
  4. 内存管理:处理大量文件时,注意内存使用,考虑分批处理
  5. 错误处理:使用 try-catch 或 @ 操作符处理可能的异常
  6. 路径安全:验证用户提供的目录路径,避免目录遍历攻击
  7. 编码处理:使用 htmlspecialchars() 输出文件名,防止 XSS 攻击

常见错误

错误 原因 解决方法
Warning: scandir(): (errno 2): No such file or directory 目录不存在 检查目录路径是否正确,确保目录存在
Warning: scandir(): failed to open dir: Permission denied 没有读取目录的权限 检查目录权限,或使用适当权限运行脚本
内存耗尽错误 目录包含大量文件,一次性加载导致内存不足 使用 opendir()/readdir() 或分批处理
包含不需要的特殊条目 没有过滤 ... 在循环中过滤这些特殊条目
性能问题 对大型目录使用默认排序 使用 SCANDIR_SORT_NONE 或 opendir()/readdir()

性能优化技巧

使用 SCANDIR_SORT_NONE

当不需要排序时,使用 SCANDIR_SORT_NONE 可以显著提高性能,特别是在处理大型目录时。

分批处理大型目录

对于非常大的目录,考虑使用 opendir()/readdir() 进行流式处理,避免内存问题。

缓存结果

如果目录内容不经常变化,可以考虑缓存扫描结果,避免重复扫描。

使用专门的迭代器

对于复杂的目录遍历需求,考虑使用 DirectoryIteratorRecursiveDirectoryIterator

相关函数