在PHP开发中,当我们面对几百兆甚至数GB的日志、导出文件或数据备份时,如果沿用读取小文件的习惯,很容易触发内存溢出。PHP默认通过memory_limit限制单个脚本占用内存,一旦把整个文件内容塞进一个字符串或数组,脚本就会被终止。要解决这个问题,核心办法是放弃一次性加载,改为分块或逐行读取,让程序在任意时刻只持有文件的一小部分数据。

为什么一次性读取会内存溢出
PHP的file_get_contents函数会把目标文件的全部字节读入内存,并赋值给一个变量。假设文件大小为2GB,而php.ini里memory_limit设为128M,函数在分配内存时就会超出限制,导致Fatal error:Allowed memory size exhausted。即便把限制调大,也意味着每个并发请求都可能吃掉数GB内存,服务器根本无法支撑多个用户同时操作。
除了内存上限,一次性读取还会带来启动延迟。文件越大,从磁盘复制到内存的时间越长,用户要等很久才能看到程序响应。而分块读取可以边读边处理,第一条数据很快就能产出,整体吞吐也更平稳。理解这一点后,我们就能明白流式读取不是优化技巧,而是处理大文件的必选项。
使用fopen和fread分块读取
最基础的分块方式是使用fopen打开文件句柄,然后在循环中用fread读取固定长度的数据块。这样内存中始终只保存一个块的内容,处理完即可丢弃或写入其他地方。
<?php
$handle = fopen('/data/big_file.log', 'rb');
if ($handle === false) {
die('无法打开文件');
}
$chunkSize = 1024 * 1024; // 每次读取1MB
while (!feof($handle)) {
$chunk = fread($handle, $chunkSize);
if ($chunk === false) {
break;
}
// 在这里处理数据块,比如解析、入库或压缩
process_chunk($chunk);
// 显式释放,避免循环内变量累积
unset($chunk);
}
fclose($handle);
function process_chunk($data) {
// 示例:简单输出长度
echo strlen($data) . ' bytes' . PHP_EOL;
}
?>
上面的代码每次只申请1MB左右的字符串,无论原文件多大,内存占用都接近恒定。注意fread的第二个参数表示最大字节数,实际读到的可能少于这个值,比如到文件末尾时。因此不能用读取长度判断结束,而要用feof确认。
这种方式的优点是粒度完全可控,适合做二进制文件复制、加密或分块压缩。缺点是如果文件是文本且按行处理更自然,手动切分块可能会把一行截成两半,需要自己维护跨块的行缓冲。对于纯文本行处理,下一节的方式更顺手。
用SplFileObject逐行遍历
PHP标准库中的SplFileObject提供了面向对象的文件遍历能力,它内部也是流式读取,但封装了逐行逻辑,不会把整个文件载入内存。
<?php
$file = new SplFileObject('/data/big_file.csv', 'r');
$file->setFlags(SplFileObject::READ_CSV);
foreach ($file as $row) {
// 每行自动解析为数组(CSV场景)
if (is_array($row)) {
handle_row($row);
}
}
function handle_row($line) {
// 示例:忽略空行
if (count(array_filter($line)) === 0) {
return;
}
// 实际业务:写入数据库等
}
?>
SplFileObject在遍历时只保存当前行的内容,内存占用极低。配合READ_CSV标志,它还能直接把逗号分隔的内容拆成数组,省去自己写解析逻辑的麻烦。对于日志分析、大CSV导入等场景,这种方式代码最简洁。
需要注意的是,如果单行本身极其长(比如一行有几十MB且没有换行),逐行读取仍可能瞬间占用较多内存。此时应回到fread定长块,并在应用层按换行符切分。选择哪种方式,取决于文件格式和单行大小分布。
常见误区与注意事项
不少人在分块读取时,会在循环里把每块数据不断拼接到一个数组或字符串,美其名曰“收集结果”,结果内存还是涨上去了。分块的意义在于处理完就释放,若必须汇总,应写入磁盘或数据库,而不是留在PHP变量里。
另一个误区是忽略文件编码与换行符。在Windows生成的文本里换行可能是rn,用fgets或SplFileObject读取时一般能正常识别,但自己用fread切块时要小心半行问题。此外,大文件读取常配合set_time_limit(0)取消执行时间限制,但也要留意php-fpm的request_terminate_timeout等外部约束。
| 方式 | 适用场景 | 内存特点 |
|---|---|---|
| file_get_contents | 小文件(<内存限制) | 峰值等于文件大小 |
| fopen+fread | 二进制或自定义块 | 恒定块大小 |
| SplFileObject | 文本行、CSV | 恒定行大小 |
总结
PHP处理大文件读不完的本质是内存模型不匹配,解决办法就是流式分块:用fopen加fread控制字节粒度,或用SplFileObject享受逐行便利。只要做到边读边处理、不累积全量数据,再大的文件也能在有限内存下平稳跑完。