PhpSpreadsheet无原生游标,需用getRowIterator()伪游标读取+XmlWriter流式导出+命令行分批处理。关键避坑:禁用toArray()、用getRealPath()、跳过空行、事务分批、禁设Content-Length。

注意:PhpSpreadsheet 本身不提供 Cursor(游标)机制,也没有类似数据库 PDO::FETCH_CURSOR 的原生游标支持。所谓“用 Cursor 处理百万级 Excel”,实际是开发者对流式读取(streaming read)或逐行迭代(row-by-row iteration)的通俗说法,并非真正意义上的数据库游标。
在 ThinkPHP 8.0 中处理百万行 Excel 数据时,关键不是找“Cursor 类”,而是避开全量加载、控制内存、分批处理。以下是真正可行、已在生产验证的核心方案:
一、导入:用 getRowIterator() 实现“伪游标”读取
避免 `$sheet->toArray()` —— 它会把整张表(含空白行、合并单元格残留)一次性载入内存,10MB 文件常导致 500MB+ 内存占用甚至进程被 kill。✅ 正确做法:
- 使用
getRowIterator()+getCellIterator()手动遍历 - 开启
setIterateOnlyExistingCells(true)跳过空单元格 - 每行即时校验、转换、入库,不缓存整表
use PhpOffice\PhpSpreadsheet\IOFactory;
$reader = IOFactory::createReader('Xlsx');
$spreadsheet = $reader->load($file->getRealPath());
$worksheet = $spreadsheet->getActiveSheet();
foreach ($worksheet->getRowIterator() as $row) {
// 跳过表头(第1行)
if ($row->getRowIndex() === 1) continue;
$cellIterator = $row->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(true);
$rowData = [];
foreach ($cellIterator as $cell) {
$rowData[] = $cell->getValue();
}
// 全空行跳过
if (empty(array_filter($rowData))) continue;
// ✅ 此处直接处理单行数据:类型转换、去重、写入DB等
$this->processOneRow($rowData);
}二、导出:不用 Spreadsheet 对象,改用底层 XmlWriter 流式写入
`new Spreadsheet()` 是内存炸弹源头。导出百万行时,必须绕过高层 API,直写 OpenXML 结构。✅ 关键动作:
立即学习“PHP免费学习笔记(深入)”;
- 不创建
Spreadsheet实例 - 用
xmlwriter_open_uri('php://output')边生成边输出 - 手动构建
sharedStrings.xml(字符串池)和worksheets/sheet1.xml - 每写完一行就
xmlwriter_flush(),并ob_flush()/flush() - 清除输出缓冲,禁用 ThinkPHP 自动响应包装
示例片段(控制器中):
ob_end_clean();
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="data.xlsx"');
$writer = xmlwriter_open_uri('php://output');
xmlwriter_start_document($writer, '1.0', 'UTF-8');
// ……后续构造 sharedStrings.xml 和 sheet1.xml(略,需按 OpenXML 规范)
// 每写一行调一次
xmlwriter_write_element($writer, 'row', $rowXml);
xmlwriter_flush($writer);
ob_flush();
flush();
// 最后 exit,防止 TP 中间件二次处理
exit;三、配套增强:命令行 + 分批 + 事务
Web 请求易超时、内存受限,百万级任务应移至命令行执行,并保障数据一致性:- 创建 ThinkPHP 命令:
php think make:command ExcelImport - 使用
array_chunk($batch, 500)分批插入 - 包裹
Db::transaction(),任一批失败则全部回滚 - 日志记录进度(如“已处理 234000/1000000 行”)
四、避坑提醒
- ❌ 不要用 `IOFactory::load($file->getPathname())` —— `getPathname()` 返回的是 ThinkPHP 封装路径,可能含中文或权限问题;务必用 `$file->getRealPath()` - ❌ 不要依赖扩展名判断格式(如 `.xlsx` 就用 `'Xlsx'`),文件损坏或伪造扩展名会导致 `Invalid reader`;优先让 `IOFactory::createReader()` 自动识别 - ❌ 导出时别设 `Content-Length` —— 流式无法预知大小,Nginx 需关闭 `fastcgi_buffering on` - ✅ 中文显示正常的关键:读取时 `setReadDataOnly(true)`,导出时设字体 `SimSun` + UTF-8 响应头不复杂但容易忽略。



















