scandir()遍历大目录易内存溢出,应改用opendir()+readdir()流式读取;拆分目录需按数量或大小分批移动文件,用rename()确保原子性,配合flock()加锁、progress.log断点续传,并预估磁盘双倍空间。

PHP 用 scandir() 遍历大文件夹容易内存溢出
直接 scandir() 一个含数万文件的目录,PHP 进程常因内存耗尽被 kill,错误类似 Fatal error: Allowed memory size of XXX bytes exhausted。这不是代码写错,而是它会一次性把所有文件名读进内存。
替代方案是用 opendir() + readdir() 流式读取:
while (false !== ($entry = readdir($dh))) {
if (in_array($entry, ['.', '..'])) continue;
// 处理单个文件/子目录
}
这样每次只存一个文件名,内存占用稳定在 KB 级别。注意:务必用 closedir($dh) 显式释放句柄,否则可能触发系统级打开文件数限制(Too many open files)。
按文件数量或大小批量拆分文件夹
“拆分”不是复制文件,而是把原目录下的内容按规则分组,生成多个子目录(如 part_001/、part_002/),再把文件移动过去。关键在于控制每批处理量:
立即学习“PHP免费学习笔记(深入)”;
- 按数量切分:每 500 个文件建一个新目录,适合文件体积较均匀的场景
- 按累计大小切分:用
filesize()累加,达到 100MB 就新建一批,适合混有大视频/小日志的目录 - 避免用
glob()或FilesystemIterator做批量操作——它们内部仍会预加载元数据,对大目录同样危险
移动文件时注意 rename() 的原子性与权限
用 rename() 移动文件比 copy() + unlink() 更安全高效,但有两个硬约束:
- 源和目标必须在同一文件系统(同分区),否则会退化为拷贝+删除,失败风险高
- PHP 进程需同时对源目录有
r-x、对目标目录有wx权限;常见错误是目标目录缺失x(执行位),导致rename(): No such file or directory - 若涉及跨用户(如 www-data 移动 root 创建的文件),需提前用
chown或chmod调整所有权,不能依赖 PHP 自动处理
实际运行前必须加锁和断点续传逻辑
大目录拆分往往耗时几分钟甚至小时,进程意外中断会导致状态不一致(部分文件已移走,但记录未保存)。简单可靠的方案是:
- 用
flock()对一个临时锁文件加写锁,防止并发执行 - 每完成一批(如 500 个文件),把当前处理到的文件名写入
progress.log,下次启动先读该文件跳过已处理项 - 禁用
set_time_limit(0)不够——还需检查max_execution_time是否被 php-fpm 或 nginx 覆盖,CLI 模式下才真正生效
最易忽略的是磁盘空间预估:移动过程临时占用双倍空间(原位置+新位置),如果只剩 2GB 却要拆分 5GB 目录,脚本会在中途因 No space left on device 失败,且难以回滚。











