最稳逐行读文件用fopen()+fgets()配合while循环,需检查fopen返回值、用$line=fgets($fp)作条件、rtrim清理换行符;超长行改用stream_get_line()限长读取;断点续传靠ftell()/fseek()记字节偏移;禁用file()避免内存溢出。

PHP fopen() + fgets() 逐行读文件最稳
直接用 while 循环读文件,核心是配合 fgets() ——它每次只读一行,内存友好,适合大文件。别一上来就 file_get_contents() 全读进内存,几 MB 的日志就可能崩掉。
常见错误是忘记检查 fopen() 返回值,结果 fgets() 在 false 上死循环;或者用 feof() 做 while 条件,导致最后一行重复读一次。
-
fopen()必须判断是否返回false,否则后续调用会警告甚至 fatal - while 条件直接写
$line = fgets($fp),靠返回false自然退出,比!feof($fp)更可靠 - 注意文件编码和换行符(
\n或\r\n),fgets()会原样保留末尾换行符,需要rtrim($line)清理
$fp = fopen('access.log', 'r');
if ($fp === false) {
die('无法打开文件');
}
while ($line = fgets($fp)) {
echo rtrim($line) . "\n";
}
fclose($fp);
大文件用 stream_get_line() 控制单次读取长度
遇到超长行(比如某条日志嵌了 base64 字符串),fgets() 可能卡住或爆内存。这时改用 stream_get_line(),能明确指定最大读取字节数,避免单行失控。
它和 fgets() 行为一致,但多了长度限制参数;不传分隔符时默认按换行切,传了就按你给的字符串切(比如读 CSV 按 , 分)。
立即学习“PHP免费学习笔记(深入)”;
- 第三个参数是分隔符,默认
\n,但必须显式传(不能省略),否则 PHP 7.4+ 会警告 - 如果某行超过设定长度,它只截取前 N 字节,不自动续读 —— 你要自己处理“未读完”的情况
- 比
fgets()略慢,但可控性强,适合解析格式混乱的日志或导入数据
$fp = fopen('huge.log', 'r');
while (($line = stream_get_line($fp, 4096, "\n")) !== false) {
echo rtrim($line) . "\n";
}
fclose($fp);
while 循环里用 ftell() 记录位置再断点续传
做日志轮询或增量同步时,不能每次从头读,得记住上次读到哪了。PHP 没有内置“文件游标持久化”,得靠 ftell() 获取当前偏移量,存到数据库或文件里。
容易踩的坑是:ftell() 返回的是字节偏移,不是行号;且在 fgets() 后调用才准,中间穿插 fseek() 或写操作会让位置错乱。
- 每次成功读完一行后立刻调用
ftell($fp),拿到的是下一行开头的字节位置 - 下次启动时用
fseek($fp, $last_pos)跳转,再开始 while 循环 - 务必在
fclose()前保存位置,否则进程崩溃就丢进度 - 注意文件被外部程序截断(如 logrotate),
fseek()到不存在的位置会失败,需加if (fseek(...) === -1)容错
别用 file() + foreach 替代 while
file() 把整个文件读成数组,每行一个元素 —— 表面看和 while 一样,实际完全不是一回事。它会一次性把所有内容加载进内存,100MB 日志直接 OOM。
有人图方便写 foreach (file('x.log') as $line),这等于先 file() 再遍历,毫无流式优势。PHP 不会帮你优化成懒加载。
-
file()默认带FILE_IGNORE_NEW_LINES和FILE_SKIP_EMPTY_LINES,但这两个标志不影响内存占用 - 即使加了
FILE_NO_DEFAULT_CONTEXT,也改不了全加载的本质 - 唯一适用场景:小配置文件(










