PHP 8.3 大数据 Excel 导入唯一可靠方案是 getRowIterator() 分块读 + setReadDataOnly(true) + 每块后立即 unset();ChunkReadFilter 因仍全程解压解析、需预读总行数且易错位,实测仅提速 12% 却大幅增加错误率与复杂度。

PHP 8.3 导入大数据量 Excel 文件,PhpSpreadsheet 默认全量加载必爆内存;唯一可靠路径是:用 getRowIterator() 流式分块 + setReadDataOnly(true) 关样式 + 每块处理完立刻 unset()。其他方案如 ChunkReadFilter 或 toArray() 都会隐式触发全量解析,实测提速仅 12%,但错误率翻倍、代码复杂度飙升。
为什么 ChunkReadFilter 在 PHP 8.3 下基本不可用
它不是跳过不读,而是“假装跳过”——ZIP 解压和 XML 解析仍全程执行,只是跳过赋值。你必须先调一次全量解析获取总行数,等于白跑一遍;合并单元格、跨行公式直接错位;还必须手动对每个 sheet 调用 getWorksheetIterator() 设置过滤器,漏一个就丢整张表。实测 10 万行文件,ChunkReadFilter 比不分块只快 12%,但内存峰值仍达 300MB+,且 getHighestRow() 内部会强制触发全量扫描,一调就崩。
getRowIterator($start, $chunkSize) 的正确用法
这是 PhpSpreadsheet 官方唯一推荐的流式读取方式,关键在三点控制:
- 必须在
IOFactory::load()后、获取工作表前,调用$reader->setReadDataOnly(true),否则字体、条件格式、公式引擎全进内存 - 对每个工作表单独调用
$worksheet->getRowIterator($startRow, $chunkSize),比如跳过标题行从第 2 行开始,每块 500 行 - 每块迭代完立即
unset($row)和unset($cell),别等 GC——PHP 8.3 的 GC 对大对象仍有延迟,不手动释放,500 行块也能撑到 128MB+ - 绝对不要调用
toArray()、getHighestRow()或getHighestColumn(),这些方法内部会强制载入整张表
字段清洗和类型转换最容易崩的几个点
用户上传的 Excel 根本不讲武德:标题带空格换行、数字转成 1.23E+10、日期存成 Excel 时间戳、空单元格返回 '' / null / 0:
立即学习“PHP免费学习笔记(深入)”;
- 标题清洗必须做:
trim(str_replace(["\n","\r","\t"], '', $header)),否则字段匹配直接失效 - 数值列不能只用
is_numeric(),要结合$cell->getDataType() === \PhpOffice\PhpSpreadsheet\Cell\Cell::TYPE_NUMERIC,再用$cell->getFormattedValue()防科学计数法失真 - 日期列必须走
\PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value),硬转int会得到 1900 年 - 空值统一转
null,尤其数据库字段为NOT NULL时,留着''插入直接报错
事务批量入库的临界值怎么卡
不是越多越好。MySQL 的 max_allowed_packet 默认 4MB,1000 行含中文约 1MB,安全余量下建议每批 500–800 行。超过这个数,INSERT INTO ... VALUES (),(),() 语句本身就会被截断或报错;低于 200 行则事务开销占比过高,性能反而下降。另外,务必在 PDO 中显式调用 beginTransaction() → 批量 execute() → commit(),别依赖自动提交。
真正难的不是写对代码,而是记住:每次调用 getWorksheetIterator()、getRowIterator() 或任何带 “get” 前缀的方法前,都得问一句——它会不会偷偷把整张表加载进内存?答案往往是“会”。



















