嘿,你好呀!今天咱们来聊聊 PHP 里一个既基础又特别实用的话题——遍历目录。
是不是有时候你写个文件管理器,或者需要批量处理某个文件夹下的图片、日志文件,结果对着满屏的 RecursiveDirectoryIterator 和 FilesystemIterator 发呆,不知道从哪儿下手?别急,作为在 PHP 坑里摸爬滚打多年的老手,我见过太多人在这儿栽跟头。
其实,PHP 提供了三种最经典、最直接的遍历方式:dir、glob 和 scandir。它们各有脾气,也各有擅长的场景。今天我就把它们掰开揉碎了讲给你听,顺便附上几个我亲手写过的实战案例,保证你看完就能用!
一、先别急着写代码,搞清楚这三兄弟的底细
1. dir() —— 老牌实干家,但有点“老气横秋”
dir() 是 PHP 早期就存在的函数,它返回一个 DirectoryIterator 对象(准确说是 Dir 类实例)。你可以把它想象成一个拿着手电筒,一步步往前走的人,每走一步,它告诉你前面是什么。
特点:
- 返回的是对象,需要手动调用
read()和close()。 - 能清晰地分辨出
.和..(虽然它们没啥用,但你知道它们在)。 - 内存占用极小,因为它是“流式”读取,不会一次性把整个目录塞进内存。
- 缺点:代码写起来略显啰嗦,而且没有原生的模式匹配(比如“只要 jpg 文件”)。
代码示例:
$handle = dir('/var/www/html/uploads');
echo "Directory handle: " . $handle->handle . "\n";
echo "Entry order: " . $handle->readingOrientation() . "\n";
while (false !== ($entry = $handle->read())) {
echo "Entry: " . $entry . "<br>";
}
$handle->close();
你看,是不是有点麻烦?每次还得记得 close(),忘了关句柄在某些极端环境下可能会出问题。不过,如果你要处理的是超大目录(比如几万个文件),dir() 的低内存优势就体现出来了。
2. glob() —— 模式匹配之王,最优雅的选择
如果说 dir() 是老实人,那 glob() 就是带点“魔法”的极客。它不仅能列目录,还能根据通配符过滤文件。
特点:
- 返回一个数组,里面的元素是匹配到的文件名(带路径)。
- 支持
*(任意字符)、?(单个字符)等通配符。 - 代码极其简洁,一个函数搞定。
- 缺点:如果目录里文件太多,它会一次性把所有匹配结果加载到内存里,可能撑爆内存。
代码示例:
// 获取当前目录下所有 .php 文件
$files = glob("*.php");
// 获取 uploads 目录下所有 .jpg 和 .png 图片(注意:glob 不支持非递归的括号扩展,除非开启 GLOB_BRACE)
// 但在 PHP 7.1+ 中,推荐使用更灵活的方式,稍后讲。
$imageFiles = glob("*.jpg", GLOB_BRACE); // 等等,GLOB_BRACE 需要配合大括号语法
更聪明的用法:
// 使用 GLOB_BRACE 匹配多种扩展名
$images = glob("*.{jpg,jpeg,png,gif}", GLOB_BRACE);
// 递归遍历(PHP 5.3+)
// 小心!这会把所有层级的文件都列出来,文件多时内存爆炸
$allFiles = glob("/var/www/html/uploads/**/*", GLOB_BRACE | GLOB_MARK);
foreach ($allFiles as $file) {
// GLOB_MARK 会在目录名后加 /
echo $file . "\n";
}
在大多数日常开发中,glob() 是我的首选,因为代码可读性太好了。
3. scandir() —— 简单粗暴的列表员
scandir() 是最直白的函数:给你一个目录路径,它还你一整排文件名。
特点:
- 返回一个数组,包含目录中的所有条目。
- 没有模式匹配功能,你要自己过滤。
- 默认按字母顺序排列(可以用
SORT_REGULAR等参数调整)。 - 同样,一次性加载所有文件到内存。
代码示例:
$files = scandir('/var/www/html/uploads');
// $files 现在是一个数组,包含 "." 和 ".."
foreach ($files as $file) {
if ($file == '.' || $file == '..') {
continue;
}
echo $file . "\n";
}
虽然它不如 glob() 灵活,但比 dir() 简单多了。如果你只需要快速列出所有文件,不在乎排序,scandir() 是个不错的选择。
二、三者深度对比:到底该选谁?
别光听我说,咱们来个实打实的对比表:
| 特性 | dir() |
glob() |
scandir() |
|---|---|---|---|
| 返回值类型 | 对象(需迭代) | 数组 | 数组 |
| 内存占用 | 极低(流式) | 高(全量加载) | 高(全量加载) |
| 模式匹配 | ❌ 不支持 | ✅ 支持通配符 | ❌ 不支持 |
| 递归遍历 | ✅ 需配合其他类 | ✅ 支持 GLOB_BRACE |
❌ 不支持 |
| 代码简洁度 | 中等 | ⭐ 最简洁 | 简单 |
| 适用场景 | 超大目录、流式处理 | 中小目录、需过滤文件类型 | 快速列出所有文件 |
我的建议:
- 日常开发:首选
glob(),代码少,逻辑清。 - 超大目录(几万个文件):用
dir()配合RecursiveDirectoryIterator,或者直接用scandir()分批处理。 - 只需要简单列出:
scandir()够用。
三、实战案例:这些坑我踩过,你别再踩
案例 1:批量压缩用户上传的图片(使用 glob)
假设你有个用户上传目录 /uploads/images,里面全是 .jpg 和 .png,现在要压缩所有图片以节省磁盘空间。
<?php
$uploadDir = '/var/www/html/uploads/images';
// 1. 使用 glob 获取所有图片文件,支持多种扩展名
$images = glob($uploadDir . "/*.{jpg,jpeg,png}", GLOB_BRACE);
if (empty($images)) {
die("目录下没有找到图片文件。\n");
}
echo "找到 " . count($images) . " 张图片,开始压缩...\n";
foreach ($images as $imagePath) {
// 获取文件扩展名
$extension = pathinfo($imagePath, PATHINFO_EXTENSION);
// 创建新的压缩图片路径
$compressedPath = $uploadDir . '/compressed_' . basename($imagePath);
try {
// 根据扩展名创建 GD 图像资源
if (in_array(strtolower($extension), ['jpg', 'jpeg'])) {
$source = imagecreatefromjpeg($imagePath);
} elseif (strtolower($extension) === 'png') {
$source = imagecreatefrompng($imagePath);
}
if (!$source) {
echo "无法读取图片: $imagePath\n";
continue;
}
// 压缩质量设为 75(平衡质量和体积)
imagejpeg($source, $compressedPath, 75);
// 清理内存
imagedestroy($source);
// 可选:删除原文件,只保留压缩后的
// unlink($imagePath);
echo "已压缩: " . basename($imagePath) . " -> " . basename($compressedPath) . "\n";
} catch (Exception $e) {
echo "处理图片时出错: $imagePath - " . $e->getMessage() . "\n";
}
}
echo "压缩完成!\n";
?>
为什么用 glob? 因为我们要找特定类型的文件,glob 的通配符功能让代码变得非常优雅。如果用 scandir(),你得在循环里自己写 pathinfo 和 in_array 判断,代码会冗长很多。
案例 2:处理超大日志目录,避免内存溢出(使用 dir 或 FilesystemIterator)
想象一下,你的服务器日志目录 /var/log/myapp 里有 5 万个 .log 文件,每个 10MB。如果用 glob() 或 scandir(),PHP 脚本会直接 OOM(内存溢出)崩掉。
这时候,dir() 或者更现代的 RecursiveDirectoryIterator 就派上用场了。虽然 dir() 是老派做法,但它概念清晰,适合理解原理。
<?php
$logDir = '/var/log/myapp';
// 使用 dir() 进行流式读取,内存占用极小
$handle = dir($logDir);
echo "开始扫描日志目录: $logDir\n";
$fileCount = 0;
while (false !== ($entry = $handle->read())) {
// 跳过 . 和 ..
if ($entry == '.' || $entry == '..') {
continue;
}
$fullPath = $logDir . '/' . $entry;
// 只处理 .log 文件
if (pathinfo($fullPath, PATHINFO_EXTENSION) === 'log') {
$fileCount++;
// 这里可以读取日志内容,进行清理或分析
// 例如:删除 30 天前的日志
$lastModified = filemtime($fullPath);
$thirtyDaysAgo = time() - (30 * 24 * 60 * 60);
if ($lastModified < $thirtyDaysAgo) {
unlink($fullPath);
echo "已删除旧日志: $entry\n";
}
}
}
$handle->close();
echo "扫描完成,共处理 $fileCount 个日志文件。\n";
?>
关键点: 这个脚本处理 5 万个文件,内存占用几乎不变,因为它只同时持有当前读取的一个文件名。如果你用 glob(),内存会瞬间飙升到数百 MB。
案例 3:递归遍历子目录,生成站点地图(使用 glob 的递归特性)
假设你是一个网站开发者,需要生成一个站点地图,列出所有 .html 页面,包括子目录中的页面。
<?php
$siteRoot = '/var/www/html';
// 使用 glob 递归获取所有 HTML 文件
// ** 表示任意层级的子目录
$htmlFiles = glob($siteRoot . '/**/*.html', GLOB_BRACE);
// 如果文件太多,可以考虑分批处理,避免内存问题
// 这里假设文件数量可控
$sitemap = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
$sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach ($htmlFiles as $file) {
// 计算相对 URL
$relativePath = str_replace($siteRoot, '', $file);
$url = 'https://www.yoursite.com' . $relativePath;
// 获取最后修改时间
$lastMod = date('Y-m-d\TH:i:sP', filemtime($file));
$sitemap .= " <url>\n";
$sitemap .= " <loc>" . htmlspecialchars($url) . "</loc>\n";
$sitemap .= " <lastmod>" . $lastMod . "</lastmod>\n";
$sitemap .= " <changefreq>weekly</changefreq>\n";
$sitemap .= " <priority>0.8</priority>\n";
$sitemap .= " </url>\n";
}
$sitemap .= '</urlset>';
// 保存站点地图
file_put_contents($siteRoot . '/sitemap.xml', $sitemap);
echo "站点地图已生成,共收录 " . count($htmlFiles) . " 个页面。\n";
?>
注意: 如果站点非常大(比如电商网站,有几十万页面),glob() 的递归可能会耗光内存。这时候应该改用 RecursiveDirectoryIterator(这是 dir() 的现代升级版,功能更强大)。
案例 4:现代 PHP 的推荐做法 —— RecursiveDirectoryIterator
既然提到了 dir() 的老派,就不能不提它的现代替代品。在实际工作中,我强烈推荐使用 RecursiveDirectoryIterator 和 RecursiveIteratorIterator 组合。它结合了 dir() 的低内存优势和 glob() 的灵活性。
<?php
$dir = new RecursiveDirectoryIterator('/var/www/html/uploads', RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $file) {
// $file 是 SplFileInfo 对象,非常强大
if ($file->isFile()) {
$path = $file->getPathname();
$ext = $file->getExtension();
// 只处理图片
if (in_array($ext, ['jpg', 'png', 'gif'])) {
// 检查文件大小,如果超过 5MB 则警告
if ($file->getSize() > 5 * 1024 * 1024) {
echo "大文件警告: $path (" . round($file->getSize() / 1024 / 1024, 2) . " MB)\n";
}
}
} elseif ($file->isDir()) {
echo "目录: " . $file->getPathname() . "\n";
}
}
?>
为什么这是最佳实践?
- 面向对象:代码更整洁。
- 功能强大:可以直接获取文件大小、修改时间、权限等信息。
- 递归支持:轻松遍历子目录。
- 内存友好:类似
dir(),按需读取,不会一次性加载整个目录树。
四、总结:如何选择?
好了,说了这么多,咱们来做个总结。下次你面对目录遍历时,可以问自己三个问题:
文件数量多吗?
- 多(几万+):用
dir()或RecursiveDirectoryIterator,避免内存溢出。 - 少(几百个):随便选,看个人喜好。
- 多(几万+):用
需要过滤特定类型的文件吗?
- 需要:用
glob(),通配符太方便了。 - 不需要:
scandir()最简单。
- 需要:用
需要递归遍历子目录吗?
- 需要:用
glob()的**模式,或者RecursiveDirectoryIterator。 - 不需要:
scandir()或dir()就够。
- 需要:用
我的个人偏好:
- 日常小脚本、快速处理:
glob() - 生产环境、大文件、长期运行的任务:
RecursiveDirectoryIterator
记住,没有最好的方法,只有最适合场景的方法。希望今天的分享能帮你理清思路,下次写代码时不再纠结!
如果你有任何问题,或者想看我演示其他 PHP 技巧,随时告诉我!咱们下期见!
