PHP:逐行读取千兆字节文件而不崩溃

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:当普通的 file_get_contents 搞垮服务器时

\n

想象一下这种情况:您正在编写日志解析器、包含数百万行数据的 CSV 导入器,或者正在处理数据库转储。您写下 $data = file_get_contents('bigfile.log'); —— 然后您的 PHP 脚本崩溃并显示错误 Fatal error: Allowed memory size exhausted。熟悉吗?如果不熟悉,那您很幸运。但迟早每个开发者都会遇到这个问题:“如何逐行读取大文件而不搞垮服务器?”

\n

在 Stack Overflow 上,问题 “How to read a large file line by line?” 获得了数十万次浏览。这并非偶然。在本文中,我们将不仅仅讨论一种方法,而是多种方法——从基础到高级。您将学习如何使用仅几兆字节的内存来读取几十吉字节大小的文件。开始吧!

\n\n

为什么 file_get_contents() 和 file() 对大文件来说是灾难

\n

在深入优化之前,让我们先弄清楚为什么标准函数不适用。

\n
    \n
  • file_get_contents() —— 将整个文件加载到字符串中。如果文件大小为 2 GB,PHP 将尝试分配 2 GB 的 RAM。在 memory_limit 为 128 MB 的主机上,这必然会导致致命错误。
  • \n
  • file() —— 也会读取整个文件,而且还会将其拆分为字符串数组。内存消耗会进一步增加(加上数组的开销)。
  • \n
\n

结论:如果文件大小超过您 memory_limit 的 10-20%,请忘记这些函数。我们需要 流式方法。

\n\n

方法 1:经典方法——fgets() 和文件描述符

\n

最基本且最可靠的方法是通过 fopen() 打开文件,然后通过 fgets() 逐行读取。这种方法使用 PHP 的内部缓冲区(通常为 8 KB),并且不会将整个文件加载到内存中。

\n\n
<?php\n$handle = fopen('bigfile.log', 'r');\nif ($handle) {\n    while (($line = fgets($handle)) !== false) {\n        // 处理行\n        echo $line; // 或者写入数据库,或者进行计数\n    }\n    fclose($handle);\n} else {\n    // 文件打开错误\n    echo "无法打开文件。";\n}\n?>
\n\n

优点:内存消耗最小,适用于任何 PHP 版本。

\n

缺点:需要显式关闭文件,没有方便的方法来跳过标题或对行进行迭代计数。

\n

只要您不尝试在循环内将所有行保存到数组中,此代码可以处理任何大小的文件。

\n\n

方法 2:SplFileObject——面向对象的方法

\n

从 PHP 5.1 开始,我们有了一个很棒的类 SplFileObject。它实现了 Iterator 和 SeekableIterator 接口,使其成为逐行读取的理想选择。

\n\n
<?php\n$file = new SplFileObject('bigfile.log');\nwhile (!$file->eof()) {\n    $line = $file->fgets();\n    // 处理行\n    echo $line;\n}\n?>
\n\n

但有一个细微之处!如果您在 foreach 内部使用 current(),SplFileObject 默认也可能将行加载到内存中。为了确保低内存消耗,请像上面那样在 while 循环中使用 fgets()。

\n\n

更高级的选项是重置标志以进行优化:

\n\n
<?php\n$file = new SplFileObject('bigfile.log');\n$file->setFlags(SplFileObject::DROP_NEW_LINE); // 从行中移除 \\n\n\nforeach ($file as $lineNum => $line) {\n    // $lineNum — 行号(从 0 开始)\n    echo "行 $lineNum: $line\\n";\n}\n?>
\n\n

重要提示:当使用 foreach 与 SplFileObject 时,PHP 仍然一次读取一行。内存不会受影响。但是,如果您尝试调用 $file->seek(1000000) 然后在文件中跳转——请小心:seek 会遍历到

博客

书籍推荐