最稳妥方式是用 fopen + fgets 逐行读取并控制循环次数,注意 trim 去换行符、false 判断防死循环、rewind 重置指针,同时校验并转换文件编码以防乱码。

用 fopen + fgets 逐行读取最稳妥
直接读整个文件再 explode("\n") 容易爆内存,尤其大文件。真实场景里,你通常只关心前 10 行日志或配置头,没必要加载全部内容。
关键点是控制循环次数,同时注意换行符兼容性(\n、\r\n 都可能):
-
fgets自动识别不同平台的换行,比file()更轻量 - 必须用
trim($line)去掉末尾换行和空格,否则判断空行会出错 - 遇到
false(EOF 或读取失败)要立即break,否则可能死循环
$fp = fopen('/path/to/log.txt', 'r');
$lines = [];
for ($i = 0; $i < 5; $i++) {
$line = fgets($fp);
if ($line === false) break;
$lines[] = trim($line);
}
fclose($fp);
大文件下避免 file() 和 file_get_contents()
这两个函数会把整个文件塞进内存,100MB 的 access.log 调用一次就可能触发 Allowed memory size exhausted 错误。
即使加了 ini_set('memory_limit', '512M'),也掩盖不了设计问题——你根本不需要后面 99% 的内容。
立即学习“PHP免费学习笔记(深入)”;
-
file()返回数组,每行一个元素,但底层仍是全量读取 -
file_get_contents()+substr()更危险:二进制截断可能切在 UTF-8 多字节中间,导致乱码 - 如果真要用字符串截断,至少先用
mb_strcut($content, 0, $pos, 'UTF-8')
用 SplFileObject 更面向对象,但要注意构造参数
它封装了底层指针操作,适合需要多次随机访问行号的场景,比如“跳过前 3 行再读 5 行”。但默认不跳过空行或 BOM,容易误判行数。
- 构造时传
SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE才能干净读取 - 调用
$obj->seek(0)不等于重置文件指针到开头,得重新 new 一个实例或用rewind() - 性能略低于原生
fgets,因为多了对象开销,小文件无感,高频调用需实测
$obj = new SplFileObject('/path/to/config.ini');
$obj->setFlags(SplFileObject::SKIP_EMPTY | SplFileObject::DROP_NEW_LINE);
$lines = [];
for ($i = 0; $i < 3 && $obj->valid(); $i++, $obj->next()) {
$lines[] = $obj->current();
}
Windows 下路径和编码最容易踩坑
不是所有服务器都用 UTF-8。特别是 Windows 上 PHP 默认用 ANSI(通常是 GBK),而日志文件可能是 UTF-8 带 BOM,fgets 读出来第一行开头就多出 \xEF\xBB\xBF,看着像乱码。
- 用
mb_detect_encoding($line, ['UTF-8', 'GBK'], true)判断后再转码 - 绝对路径推荐用
__DIR__ . '/data/file.txt',避免./在 CLI 和 Web 环境下工作目录不同 - 打开文件前加
if (!is_readable($path)) { throw new RuntimeException("Cannot read $path"); },别让fopen静默失败











