核心解法是用 fgetcsv() + fopen() 逐行流式读取,避免全量加载;需用生成器封装、正确处理异常与文件关闭、批量插入控制大小并包裹事务,导出时分片 zip 打包。

直接用 file_get_contents() 或 file() 读取百万行 CSV/日志文件,99% 会触发 Fatal error: Allowed memory size exhausted。这不是配置调高就能解决的问题——PHP 数组本身有巨大元数据开销,几百万行解析后轻松吃掉 3–5GB 内存。核心解法只有一个:不把数据“装进来”,而是“流着用”。
用 fgetcsv() + fopen() 逐行读,别碰全量加载
这是最基础也最容易被跳过的防线。很多开发者看到“CSV”就条件反射写 str_getcsv(file_get_contents(...)),结果一跑就崩。
-
fgetcsv()是 C 层实现的,底层用固定缓冲区,每调用一次只解析当前行,内存占用恒定在 KB 级 - 必须配
fopen()打开文件句柄,不能和file_get_contents()混用——后者已把整个文件塞进内存,fgetcsv()再调也没意义 - 注意 CSV 中可能含换行符(字段内换行),
fgetcsv()默认能正确处理,但需确保没手动改过ini_set('auto_detect_line_endings', '1')导致解析错位 - Windows 行尾
\r\n、Linux\n、Mac\r都兼容,无需额外 normalize
用生成器封装读取逻辑,避免闭包/全局变量污染
单纯 while 循环读取没问题,但一旦要复用(比如同时读多个文件、或加过滤条件),逻辑会迅速变臃肿。生成器不是炫技,是让“流”可组合的关键。
- 函数签名必须带类型声明:
function readCsv(string $path): \Generator,PHP 8.1+ 能提前捕获路径类型错误 - 不要在生成器里做数据库插入或 HTTP 请求——yield 出去后再统一处理,否则无法控制批量节奏
- 如果 CSV 有 header 行,第一轮
fgetcsv()后立刻yield字段名,后续 yield 数据行,调用方用current()和next()区分更清晰 - 生成器内部
fclose()必须放在finally块里,否则异常中断会导致文件句柄泄漏
批量写入数据库时,INSERT ... VALUES (...), (...) 要控制单条语句长度
很多人知道要批量插,但批量到 10 万行一条 SQL,MySQL 会直接报 Packet too large 错误,PHP 层还收不到明确提示,只显示连接断开。
- 安全上限看
max_allowed_packet配置,默认通常 4MB,按每行平均 200 字节算,最多插 2 万行左右 - 推荐固定
$batchSize = 1000,实测在多数 MySQL 版本和网络环境下最稳 - 不要用
PDO::prepare()循环 execute——预处理只省解析,不省网络往返;拼接多值 INSERT 才真正减少 I/O - 事务要包住整个 batch:
beginTransaction()→ 插入 →commit(),否则每条 insert 都是独立事务,性能暴跌
导出大 Excel 时,别生成单个文件,用 ZIP 分片打包
PhpSpreadsheet 之类库对单文件行数有限制(如 .xlsx 最多 1048576 行),且内存随行数线性增长。强行突破只会 OOM。
- 按 5 万行/文件切分,生成
export_001.xlsx、export_002.xlsx… 临时文件 - 用
ZipArchive添加文件时,调用$zip->addFile($tmpPath, "data_{$i}.xlsx"),不要addFromString()——后者会把内容再拷贝进内存 - ZIP 打包完立即
unlink()临时 Excel 文件,否则磁盘空间会被占满 - 最后用
readfile()输出 ZIP 流,配合header('Content-Transfer-Encoding: binary'),避免 PHP 输出缓冲干扰
真正容易被忽略的点是:流式处理不等于“不关心状态”。比如 CSV 中某行解析失败,fgetcsv() 返回 false,但很多人没检查就继续循环,导致后续所有行偏移错位。每一层流都要有明确的错误边界,而不是靠“应该不会出错”来赌。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











