提到文件操作,很多刚学 PHP 的朋友都会觉得头疼,尤其是涉及目录遍历的时候。今天咱们就坐下来,像朋友聊天一样,把这五种最常用、也最容易混淆的方法掰开了、揉碎了讲清楚。我会用大白话,配上能直接跑的代码,保证你看完就能用上。
一、最经典的 opendir + readdir:老派但稳妥
这是 PHP 最古老的目录遍历方式,源自 C 语言风格的流处理。它不像数组那样一次性把所有东西加载到内存,而是“边读边处理”,所以对于大目录非常友好。
1. 基本用法
<?php
$dir = '/path/to/your/directory';
// 打开目录句柄
if ($handle = opendir($dir)) {
// 循环读取目录项
while (false !== ($file = readdir($handle))) {
// '.' 和 '..' 是保留项,通常要跳过
if ($file !== '.' && $file !== '..') {
echo $file . PHP_EOL;
}
}
// 记得关闭句柄,防止资源泄漏
closedir($handle);
} else {
echo "无法打开目录:$dir";
}
2. 进阶技巧:获取完整路径
上面那段代码只输出了文件名,但很多时候你需要完整的文件路径。我们可以加上 realpath() 或者手动拼接:
<?php
$dir = '/path/to/your/directory';
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if ($file !== '.' && $file !== '..') {
$fullPath = $dir . '/' . $file;
// 判断是文件还是目录
if (is_dir($fullPath)) {
echo "[目录] $fullPath\n";
} else {
echo "[文件] $fullPath\n";
}
}
}
closedir($handle);
}
3. 为什么推荐这种写法?
- 内存占用极低:它不会把整个目录树加载到内存,哪怕你有几万个子目录,也只处理当前这一层。
- 兼容性好:几乎所有 PHP 环境都支持。
- 灵活:你可以在读取每个文件时即时处理,比如边读边写入数据库。
4. 一个小坑
注意 while (false !== ($file = readdir($handle))) 这个写法。如果你写成 while ($file = readdir($handle)),当文件名恰好是 0 或空字符串时,循环会提前终止。所以一定要用严格比较 !== false。
二、glob():简洁的“文件名匹配”神器
glob() 是一个被严重低估的函数。它不仅能列出文件,还能通过通配符过滤,特别适合那些“我只想要特定类型文件”的场景。
1. 基本用法
<?php
$files = glob('/path/to/directory/*');
// 输出所有文件(不包含子目录)
foreach ($files as $file) {
echo $file . PHP_EOL;
}
2. 加上通配符过滤
这是 glob() 真正的威力所在:
<?php
// 只获取所有 .jpg 文件
$images = glob('/path/to/directory/*.jpg');
// 只获取所有 .php 文件
$phpFiles = glob('/path/to/directory/*.php');
// 获取所有文件和目录(用 *)
$allItems = glob('/path/to/directory/*');
foreach ($images as $img) {
echo "图片:$img\n";
}
3. 递归遍历:glob 配合标志
如果你想让 glob() 也支持子目录,可以加上 GLOB_BRACE 或递归标志。但要注意,glob() 原生不支持递归,你需要自己处理:
<?php
function globRecursive($pattern, $flags = 0) {
$files = glob($pattern, $flags);
// 如果匹配到了目录,继续递归
foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR) as $dir) {
$files = array_merge(
$files,
globRecursive($dir . '/' . basename($pattern), $flags)
);
}
return $files;
}
// 使用示例:递归查找所有 .txt 文件
$allTxtFiles = globRecursive('/path/to/directory/**/*.txt', GLOB_BRACE);
foreach ($allTxtFiles as $file) {
echo "找到文本文件:$file\n";
}
4. glob() 的优缺点
- 优点:代码极简,支持通配符,内置过滤。
- 缺点:会把所有匹配结果一次性加载到数组中,如果文件特别多(比如几万个),内存占用会很高。另外,它不能直接告诉你哪些是目录、哪些是文件,需要额外判断。
三、scandir():最简单的“一键列出”
如果你只是想要一个目录下的所有文件和子目录列表,scandir() 是最省事的。它直接返回一个数组,不用开句柄、不用循环读取。
1. 基本用法
<?php
$items = scandir('/path/to/directory');
// 默认是升序排列
foreach ($items as $item) {
// 跳过 . 和 ..
if ($item !== '.' && $item !== '..') {
echo $item . PHP_EOL;
}
}
2. 倒序排列
<?php
// 传入第二个参数 SORT_DESC,实现倒序
$items = scandir('/path/to/directory', SORT_DESC);
foreach ($items as $item) {
if ($item !== '.' && $item !== '..') {
echo $item . PHP_EOL;
}
}
3. 快速判断文件还是目录
<?php
$dir = '/path/to/directory';
$items = scandir($dir);
$files = [];
$directories = [];
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_dir($path)) {
$directories[] = $item;
} else {
$files[] = $item;
}
}
echo "文件数:" . count($files) . PHP_EOL;
echo "目录数:" . count($directories) . PHP_EOL;
4. scandir() 的注意事项
- 返回点:它会返回
.和..,一定要过滤掉,否则后续路径拼接会出问题。 - 排序问题:默认是字母升序,如果你需要按时间或其他规则排序,得自己处理。
- 内存问题:和大目录一样,它会把所有项加载到数组,所以大目录慎用。
四、RecursiveDirectoryIterator:OOP 风格的优雅方案
从 PHP 5.0 开始, SPL(Standard PHP Library)提供了面向对象的目录遍历方式。RecursiveDirectoryIterator 是其中的明星,它配合 RecursiveIteratorIterator,能写出非常优雅且高效的递归代码。
1. 基础递归遍历
<?php
$dir = '/path/to/directory';
// 创建递归目录迭代器
$iterator = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS);
// 包装成递归迭代器,自动处理子目录
$recursiveIterator = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursiveIterator as $file) {
// $file 是 SplFileInfo 对象
echo $file->getPathname() . PHP_EOL;
}
2. 只处理文件,忽略目录
<?php
$dir = '/path/to/directory';
$iterator = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS);
$recursiveIterator = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($recursiveIterator as $file) {
// 这里只遍历到叶子节点(即文件)
echo $file->getPathname() . PHP_EOL;
}
3. 过滤特定类型文件
<?php
$dir = '/path/to/directory';
$iterator = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS);
$recursiveIterator = new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::LEAVES_ONLY);
// 用 RegexIterator 过滤
$regex = new RegexIterator($recursiveIterator, '/\.jpg$/i');
foreach ($regex as $file) {
echo $file->getPathname() . PHP_EOL;
}
4. 获取文件大小、修改时间等信息
<?php
$dir = '/path/to/directory';
$iterator = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS);
$recursiveIterator = new RecursiveIteratorIterator($iterator);
foreach ($recursiveIterator as $file) {
if ($file->isFile()) {
echo "文件:" . $file->getFilename() . "\n";
echo " 大小:" . $file->getSize() . " 字节\n";
echo " 修改时间:" . date('Y-m-d H:i:s', $file->getMTime()) . "\n";
}
}
5. 为什么推荐这种方式?
- 内存友好:和
opendir一样,它也是流式读取,不会一次性加载所有文件。 - 功能强大:可以直接获取文件的元数据(大小、时间、权限等)。
- 可过滤:配合
RegexIterator、CallbackFilterIterator等,可以灵活地筛选文件。 - 面向对象:代码结构清晰,易于维护和扩展。
五、自定义递归函数:灵活可控的终极方案
有时候,你需要更细粒度的控制,比如自定义过滤逻辑、处理异常、或者按照特定顺序遍历。这时候,写一个递归函数是最灵活的选择。
1. 基础递归函数
<?php
function scanDirectory($dir, &$results = []) {
// 检查目录是否存在
if (!is_dir($dir)) {
return $results;
}
// 读取目录内容
$items = scandir($dir);
foreach ($items as $item) {
// 跳过 . 和 ..
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_dir($path)) {
// 如果是目录,递归调用
scanDirectory($path, $results);
} else {
// 如果是文件,加入结果数组
$results[] = $path;
}
}
return $results;
}
// 使用示例
$allFiles = scanDirectory('/path/to/directory');
print_r($allFiles);
2. 带过滤条件的递归函数
<?php
function scanDirectoryFiltered($dir, &$results = [], $filter = null) {
if (!is_dir($dir)) {
return $results;
}
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_dir($path)) {
scanDirectoryFiltered($path, $results, $filter);
} else {
// 如果有过滤条件,应用过滤
if ($filter && !$filter($path)) {
continue;
}
$results[] = $path;
}
}
return $results;
}
// 使用示例:只获取 .jpg 文件
$jpgFiles = scanDirectoryFiltered(
'/path/to/directory',
[],
function($path) {
return preg_match('/\.jpg$/i', $path);
}
);
print_r($jpgFiles);
3. 处理权限错误
在实际项目中,你可能会遇到权限不足的目录,导致 scandir() 报错。我们可以加入错误处理:
<?php
function scanDirectorySafe($dir, &$results = [], $ignoreErrors = true) {
if (!is_dir($dir)) {
return $results;
}
// 尝试读取目录,捕获可能的权限错误
@set_error_handler(function($errno, $errstr) use ($ignoreErrors) {
if (!$ignoreErrors) {
throw new Exception($errstr);
}
});
$items = @scandir($dir);
if ($items === false) {
// 恢复错误处理
restore_error_handler();
return $results;
}
restore_error_handler();
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$path = $dir . '/' . $item;
if (is_dir($path)) {
scanDirectorySafe($path, $results, $ignoreErrors);
} else {
$results[] = $path;
}
}
return $results;
}
六、五种方法对比总结
| 方法 | 代码复杂度 | 内存占用 | 递归支持 | 过滤能力 | 适用场景 |
|---|---|---|---|---|---|
| opendir/readdir | 中等 | 低 | 需手动递归 | 弱 | 大目录、内存敏感 |
| glob() | 低 | 高 | 需手动递归 | 强(通配符) | 小目录、文件匹配 |
| scandir() | 极低 | 高 | 需手动递归 | 弱 | 快速列出文件 |
| RecursiveDirectoryIterator | 中等 | 低 | 原生支持 | 强(可组合) | 生产环境、复杂逻辑 |
| 自定义递归函数 | 高 | 中 | 完全自定义 | 完全自定义 | 特殊需求、教学 |
七、常见坑点与注意事项
1. 路径分隔符问题
在不同操作系统上,路径分隔符不同。Windows 用 \,Unix/Linux 用 /。建议在拼接路径时使用 DIRECTORY_SEPARATOR 或 realpath():
<?php
// 不推荐:硬编码路径分隔符
$path = $dir . '\images'; // Windows 下可能出错
// 推荐:使用 DIRECTORY_SEPARATOR
$path = $dir . DIRECTORY_SEPARATOR . 'images';
// 更推荐:使用 realpath() 规范化路径
$path = realpath($dir . DIRECTORY_SEPARATOR . 'images');
2. 隐藏文件
在 Unix/Linux 系统中,以 . 开头的文件是隐藏文件。scandir() 和 opendir() 都会返回它们,而 glob() 默认不会。如果你需要处理隐藏文件,要小心过滤:
<?php
// scandir 会返回 .htaccess、.git 等隐藏文件
$items = scandir($dir);
foreach ($items as $item) {
if (substr($item, 0, 1) === '.') {
// 这是隐藏文件,决定是否跳过
continue;
}
// 处理其他文件
}
3. 符号链接(Symlink)
在某些系统中,目录可能包含符号链接。默认情况下,PHP 会跟随符号链接,这可能导致无限递归或重复处理。你可以通过 RecursiveDirectoryIterator 的标志来控制:
<?php
// 不跟随符号链接
$iterator = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::NO_FOLLOW_SYMLINKS);
4. 编码问题
如果你的文件名包含中文或其他非 ASCII 字符,确保 PHP 内部编码和文件系统编码一致,否则会出现乱码:
<?php
// 设置 mb_internal_encoding
mb_internal_encoding('UTF-8');
// 或者在文件头声明
header('Content-Type: text/html; charset=UTF-8');
5. 性能优化建议
- 大目录用迭代器:
RecursiveDirectoryIterator或opendir是最佳选择。 - 小目录用 scandir/glob:代码简洁,开发效率高。
- 避免递归过深:如果目录层级超过 10 层,考虑限制递归深度,防止栈溢出。
- 缓存结果:如果目录结构不常变化,可以将遍历结果缓存到文件或数据库中,避免重复遍历。
八、实战示例:一个简单的文件管理器
让我们把学到的知识结合起来,写一个实用的文件管理器:
”`php <?php class FileManager {
private $rootDir;
private $filterCallback;
public function __construct($rootDir, $filter = null) {
$this->rootDir = realpath($rootDir);
$this->filterCallback = $filter;
}
/**
* 递归遍历目录,返回文件列表
*/
public function getFiles($includeHidden = false) {
$files = [];
$this->scanDirectory($this->rootDir, $files, $includeHidden);
return $files;
}
private function scanDirectory($dir, &$files, $includeHidden) {
$items = scandir($dir);
foreach ($items as $item) {
// 跳过 . 和 ..
if ($item === '.' || $item === '..') {
continue;
