ThinkPHP导入Excel速度慢的根本原因是默认加载器不适用于大数据场景,应改用Box/Spout流式解析器,配合临时文件持久化、禁用日期格式化、逐批插入与手动GC回收,并优先采用CSV格式上传以控制内存。

ThinkPHP导入Excel速度慢,常表现为10万行数据耗时几分钟、内存爆掉、PHP进程被kill或超时中断——这不是代码写法有误,而是默认加载方式与大数据场景严重 mismatch。
替换默认加载器:弃用IOFactory::load(),改用流式解析器
第一步:卸载phpspreadsheet(若已安装),执行:composer remove phpoffice/phpspreadsheet
第二步:安装轻量级流式解析库box/spout:composer require box/spout:^4.0。它不依赖DOM解析,不加载整表进内存,5MB Excel仅占3MB内存峰值。
第三步:在控制器中引入并初始化:use Box\Spout\Reader\Common\Creator\ReaderFactory; → $reader = ReaderFactory::create(Type::XLSX);
立即学习“PHP免费学习笔记(深入)”;
【必须关闭样式解析】 Spout默认不读样式和公式,但若Excel含大量条件格式或宏,仍可能触发隐藏开销。调用$reader->setShouldFormatDates(false)禁用日期自动转换,避免内部反复调用Carbon实例。
上传路径处理:绕过临时文件清理陷阱
ThinkPHP上传后会自动清理$_FILES['file']['tmp_name']路径,而IOFactory::load()常在此之后才调用,导致File does not exist错误。
方法一:立即保存临时文件到持久目录
调用$file->move(ROOT_PATH . 'public' . DS . 'uploads', true),返回真实路径$realPath,后续全部基于该路径操作。
方法二:直接使用资源流(推荐)$fp = fopen($file->getRealPath(), 'r'); → $reader->open($fp);。此方式跳过文件系统IO,规避权限与路径编码问题。
【严禁使用$file->getPathname()】 它返回客户端原始路径(如C:\Users\abc.xlsx),服务端根本不可访问,一用即报错。
逐行解析+批量写入:控制内存与数据库压力
① 打开文件后,立即跳过表头:$reader->skipRows(1);
② 使用foreach ($reader->getSheetIterator() as $sheet)遍历工作表,对每个表单独处理。
③ 每500行组装一次批量插入SQL:Db::name('user')->insertAll($batchData);,避免单条INSERT压垮连接池。
④ 每批处理完手动释放变量:unset($batchData); gc_collect_cycles();。PHP 8.3对Spout对象的GC滞后明显,不主动回收会导致内存持续上涨。
这一步操作起来很简单,直接把$batchData = []放在循环外,每次array_push($batchData, $row)即可。但注意:若Excel列顺序与数据库字段不一致,必须提前做字段映射,否则insertAll会因键名缺失报错。
前端预处理:让Excel转CSV再提交
方法一:前端用SheetJS转换
引入xlsx.full.min.js → const csv = XLSX.utils.sheet_to_csv(workbook.Sheets[workbook.SheetNames[0]]); → 以text/csv类型提交。
方法二:后端接收后直连fgetcsv$handle = fopen($file->getRealPath(), 'r'); while (($row = fgetcsv($handle)) !== false) { ... }。100万行内存恒定在12KB以内。
注意CSV BOM头:若前端未strip BOM,需先$firstLine = fgets($handle); if (substr($firstLine, 0, 3) === "\xEF\xBB\xBF") { /* 跳过 */ },否则首列字段名带乱码。



















