
用 scandir() + 手动过滤最可控
PHP 没有内置“排除某类扩展名”的目录遍历函数,scandir() 返回所有条目,后续靠 pathinfo() 或字符串判断来筛。好处是逻辑透明、兼容性好(PHP 4+ 都行),坏处是你得自己写过滤条件。
常见错误是只检查文件名后缀而忽略大小写,比如 .JPG 被漏掉;或者没跳过 . 和 .. 导致递归出错。
- 先用
scandir($dir)获取全部项,再array_filter()处理 - 对每个项调用
is_file($full_path)确保是文件(避免把子目录当文件过滤) - 用
strtolower(pathinfo($file, PATHINFO_EXTENSION))统一小写再比对,避免大小写陷阱 - 排除数组里的
'.'和'..'—— 这俩不是文件也不是真实目录项,只是导航符号
RecursiveDirectoryIterator 配合 accept() 方法更面向对象
如果要递归遍历子目录,且想在迭代器层面就过滤掉不需要的扩展名,RecursiveDirectoryIterator 是更干净的选择。它本身不支持直接传入黑名单,但可以继承并重写 accept() 方法。
容易踩的坑是:忘记调用父类的 accept() 导致连目录都不进;或者在 accept() 里用了 file_exists() 或 is_file(),其实迭代器已经提供了 $this->isFile(),更轻量。
立即学习“PHP免费学习笔记(深入)”;
- 只在
$this->isFile() === true时检查扩展名,跳过目录和特殊项 - 黑名单用
in_array(strtolower($ext), $blacklist)判断,别用strpos()防止.tar.gz误匹配.tar - 构造
RecursiveIteratorIterator时加RecursiveIteratorIterator::LEAVES_ONLY,避免返回空目录节点
用 glob() 快速单层过滤,但不支持递归和复杂逻辑
glob() 写起来最短,比如 glob($dir . '/*.php') 只取 PHP 文件。但它天生只能“白名单”式匹配,要“排除”就得绕一下:先取全部,再取要排除的,最后用 array_diff()。
性能上,glob() 在大目录里可能比 scandir() 慢,因为 shell 层做了通配展开;而且 Windows 下对大小写不敏感,Linux 下敏感,行为不一致。
- 单层过滤推荐:
array_diff(glob($dir . '/*'), glob($dir . '/*.log'), glob($dir . '/*.tmp')) - 别对
glob($dir . '/*')结果直接foreach后用is_dir()判,那会多一次系统调用 - 路径末尾必须带
/,否则$dir . '*.php'在$dir无结尾斜杠时会拼错
扩展名判断别硬编码 substr(),用 pathinfo() 更稳
有人用 substr($file, -4) === '.php',这在 index.php.bak 上就挂了。还有人用 explode('.', $file) 取最后一段,遇到 archive.tar.gz 就返回 gz 而不是 tar.gz。
pathinfo() 是唯一能正确处理多点分隔、隐藏文件(如 .gitignore)、无扩展名文件的方案。注意它返回的是关联数组,键名固定,别拼错 PATHINFO_EXTENSION。
- 始终用
pathinfo($file, PATHINFO_EXTENSION),不要自己切字符串 - 空扩展名(如
/bin/sh)返回空字符串,检查时用=== '',别用empty() - 如果文件路径含中文或特殊字符,确保脚本和文件系统编码一致,否则
pathinfo()可能返回乱码扩展名
真正麻烦的不是怎么写过滤逻辑,而是目录里混着软链接、权限受限文件、正在被写的临时文件——这些在 is_file() 或 stat() 阶段就可能抛 Warning,得提前用 @ 抑制或 clearstatcache() 配合处理。实际跑的时候,永远比本地测试多出三类意外。









