引言:当普通的 file_get_contents 搞垮服务器时
\n想象一下这种情况:您正在编写日志解析器、包含数百万行数据的 CSV 导入器,或者正在处理数据库转储。您写下 $data = file_get_contents('bigfile.log'); —— 然后您的 PHP 脚本崩溃并显示错误 Fatal error: Allowed memory size exhausted。熟悉吗?如果不熟悉,那您很幸运。但迟早每个开发者都会遇到这个问题:“如何逐行读取大文件而不搞垮服务器?”
在 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
结论:如果文件大小超过您 memory_limit 的 10-20%,请忘记这些函数。我们需要 流式方法。
\n\n方法 1:经典方法——fgets() 和文件描述符
\n最基本且最可靠的方法是通过 fopen() 打开文件,然后通过 fgets() 逐行读取。这种方法使用 PHP 的内部缓冲区(通常为 8 KB),并且不会将整个文件加载到内存中。
<?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 接口,使其成为逐行读取的理想选择。
<?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<?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 会遍历到