直接使用file_get_contents()读取大日志文件会爆内存,因其一次性加载全部内容;应改用fopen()+fgets()+yield实现逐行流式读取,每次仅驻留一行于内存,避免内存溢出。

为什么直接 file_get_contents() 会爆内存
读取几 GB 的日志文件时,file_get_contents() 会把整个文件一次性载入内存,PHP 进程很可能触发 Allowed memory size exhausted 错误。即使你只关心某几行(比如含 "ERROR" 的记录),也没必要全加载——关键在于“按需获取”,而非“全量加载”。
yield 生成器正是为此而生:它不返回数组,而是返回一个可遍历的 Generator 对象,每次迭代只读一行、处理一行、释放前一行的内存。
用 fopen() + yield 实现真正的逐行流式读取
不能依赖 file() 或 file_get_contents() 的变体(如 stream_get_line() 配合 while),它们仍可能因缓冲或内部实现导致隐式内存累积。稳妥做法是手动控制文件指针:
-
fopen()打开文件,模式必须是"r"(非二进制) - 循环中用
fgets()读取单行,它天然按换行符截断,且不缓存全文 - 每读一行就
yield一次,函数立即挂起,内存只保留当前行字符串 - 记得在生成器结束后
fclose(),但 PHP 会在资源销毁时自动关闭,显式调用更可控
示例函数:
立即学习“PHP免费学习笔记(深入)”;
function readLogFile(string $path): Generator
{
$fp = fopen($path, 'r');
if (!$fp) {
throw new RuntimeException("Cannot open {$path}");
}
try {
while (($line = fgets($fp)) !== false) {
yield rtrim($line, "\r\n"); // 去掉换行符,保持原始内容干净
}
} finally {
fclose($fp);
}
}
实际使用时要注意的三个坑
生成器本身不执行逻辑,只有被遍历时才触发读取。这意味着:
- 不能对生成器直接调用
count()—— 它不是数组,没有长度;若需行数,得遍历计数或用wc -l外部命令预估 - 生成器只能遍历一次;重复
foreach会静默无输出,需重新调用函数获得新生成器实例 - 日志文件若被外部进程持续追加(如
tail -f场景),fgets()在 EOF 后会返回false并结束,不会自动等待新行;如需实时监听,得配合feof()+sleep()+ 重试逻辑,或改用stream_select()
性能对比:yield vs SplFileObject
SplFileObject 看似更“面向对象”,但它在迭代过程中仍会做额外封装(如行号计数、键值映射),实测在大文件下比裸 fgets() + yield 慢 10%–15%,内存占用也略高。PHP 7.4 中,原生 fgets() 是最轻量、最可预测的逐行读取方式。
真正影响效率的,往往不是生成器语法,而是后续处理逻辑——比如你在 yield 后立刻做 json_decode() 或正则匹配,这些操作本身才是瓶颈。先确保读取层不拖后腿,再优化业务解析部分。
文件编码不一致(如混合 UTF-8 和 GBK)时,fgets() 不做转码,你得自己用 mb_convert_encoding() 处理,否则中文可能乱码——这点容易被忽略,尤其跨平台日志场景。



















