在PHP中读取非常大的文件

30

fopen在我尝试读取一个适度大小的文件时失败了,PHP中的一个6兆文件让它无法工作,虽然大小在100k左右的小文件却没问题。我已经阅读到有时需要重新编译PHP并加上-D_FILE_OFFSET_BITS=64标志才能读取超过20 GB或其他荒谬的东西的文件,但是对于一个6兆的文件,我不应该遇到问题吧?最终,我们将想要读取大约100兆的文件,并且能够像处理较小文件一样通过fgets逐行读取文件将是很好的。

您在PHP中读取和操作非常大的文件的技巧/解决方案是什么?

更新:这里是一个简单的代码块示例,在我的6兆文件上失败-PHP似乎没有抛出错误,而是返回false。也许我在做一些非常愚蠢的事情吗?

$rawfile = "mediumfile.csv";

if($file = fopen($rawfile, "r")){  
  fclose($file);
} else {
  echo "fail!";
}

另一个更新:感谢所有人的帮助,最终问题原来是一个权限问题,非常愚蠢。我的小文件莫名其妙地有了读取权限,而更大的文件却没有。天啊!


你只是想传输文件吗?比如下载?还是你实际上正在解析文件中的数据以达到某种目的?谢谢。 - DreamWerx
请确保在不生成警告/错误的情况下运行。请使用error_reporting(E_ALL)打开所有错误,并确保display_errors设置为on以在浏览器中显示,或者检查您的Web服务器错误日志。 - Philip Reynolds
8个回答

57
你确定是fopen出了问题,而不是你脚本的超时设置?默认情况下通常为30秒左右,如果你的文件读取时间超过了这个时间,可能会导致超时。
另一个需要考虑的因素可能是脚本的内存限制-将文件读入数组可能会触发此限制,因此请检查错误日志以获取内存警告。
如果以上两个方法都不行,你可以考虑使用fgets逐行读取文件,并在处理过程中进行处理。
$handle = fopen("/tmp/uploadfile.txt", "r") or die("Couldn't get handle");
if ($handle) {
    while (!feof($handle)) {
        $buffer = fgets($handle, 4096);
        // Process buffer here..
    }
    fclose($handle);
}

编辑

PHP似乎不会抛出错误,只是返回false。

$rawfile的路径是否相对于脚本运行的位置正确?也许在这里为文件名设置绝对路径会有所帮助。


4
这是一个打开非常大的文件的可能解决方案。我正在使用这种方法来处理1.5GB的文件,没有任何问题。所有其他的解决方案,比如file_get_contents或file,都会将整个文件读入内存。这种方法则逐行进行处理。 - StanleyD
为什么4096意味着一行? - Phoenix
@Phoenix 4096 表示,最多读取 4096 - 1 字节,如果没有遇到换行符。请查看手册。 - a3f
3
对我而言,stream_get_linefgets 更快,可以查看这个比较:https://gist.github.com/joseluisq/6ee3876dc64561ffa14b - joseluisq

13

进行了1.3GB和9.5GB文件的两次测试。

1.3 GB

使用fopen()

该过程在计算中使用了15555毫秒。

它花费了169毫秒在系统调用上。

使用file()

该过程在计算中使用了6983毫秒。

它花费了4469毫秒在系统调用上。

9.5 GB

使用fopen()

该过程在计算中使用了113559毫秒。

它花费了2532毫秒在系统调用上。

使用file()

该过程在计算中使用了8221毫秒。

它花费了7998毫秒在系统调用上。

看起来file()更快。


9

• 当文本文件超过20MB时,使用fgets()函数会导致解析速度大幅降低。

• 当文本文件在40MB以内时,file_get_contents()函数能够给出良好的结果,而在100MB以内则能够接受。但是,file_get_contents()会将整个文件加载到内存中,因此它不具备可扩展性。

• 对于大型文本文件,file()函数效果非常糟糕,因为该函数会创建一个包含每行文本的数组,这样的数组会被存储在内存中,所需的内存更多。
实际上,对于一个200MB的文件,我只能将memory_limit设置为2GB来解析,这对于我打算解析的1GB以上的文件是不合适的。

当您需要解析大于1GB的文件且解析时间超过15秒,并且希望避免将整个文件加载到内存中时,您需要找到另一种方法。

我的解决方案是将数据分成任意大小的块进行解析。代码如下:

$filesize = get_file_size($file);
$fp = @fopen($file, "r");
$chunk_size = (1<<24); // 16MB arbitrary
$position = 0;

// if handle $fp to file was created, go ahead
if ($fp) {
   while(!feof($fp)){
      // move pointer to $position in file
      fseek($fp, $position);

      // take a slice of $chunk_size bytes
      $chunk = fread($fp,$chunk_size);

      // searching the end of last full text line (or get remaining chunk)
      if ( !($last_lf_pos = strrpos($chunk, "\n")) ) $last_lf_pos = mb_strlen($chunk);

      // $buffer will contain full lines of text
      // starting from $position to $last_lf_pos
      $buffer = mb_substr($chunk,0,$last_lf_pos);
      
      ////////////////////////////////////////////////////
      //// ... DO SOMETHING WITH THIS BUFFER HERE ... ////
      ////////////////////////////////////////////////////

      // Move $position
      $position += $last_lf_pos;

      // if remaining is less than $chunk_size, make $chunk_size equal remaining
      if(($position+$chunk_size) > $filesize) $chunk_size = $filesize-$position;
      $buffer = NULL;
   }
   fclose($fp);
}

使用的内存仅为$chunk_size,速度略低于使用file_get_contents()。我认为PHP Group应该采用我的方法来优化其解析函数。
*) 在此处找到get_file_size()函数。这里

1
这是不完整的,fread会移动文件指针。如果不重置位置,你将丢失第一个大块,16mb。先进行测试。 - ion
感谢Ionut提供有用的观察。代码已更新。 - Tinel Barb
我尝试使用一个大文件(大约256MB)来进行操作,但是循环似乎在缓冲区的最后一部分卡住了。缓冲区的最后< 16MB 部分似乎只包含1行内容,因此它逐行读取,需要很长时间才能完成。 - GerritElbrink

1

如果你只想输出文件,那么你可以尝试使用readfile函数。

如果不是这种情况,也许你应该考虑一下应用程序的设计,为什么要在Web请求中打开如此大的文件呢?


我们必须自动化添加大量数据,以便用户可以上传大型CSV文件,并由应用程序解析和集成到数据库中。如果您认为使用PHP读取和解析上传的文件不是最佳方法,我很乐意听取其他建议。 - user5564
我不认为PHP处理6MB的csv文件会有问题?对于它来说,这似乎是一个足够小的文件。根据上面的评论,请发布确切的错误和/或代码。你可能遇到了内存错误?或者是max_execution_time?我们需要更多信息来帮助你。 - DreamWerx

1
我使用fopen打开视频文件进行流媒体传输,使用PHP脚本作为视频流服务器。对于大小超过50/60 MB的文件没有任何问题。

0

对我来说,fopen() 在处理超过1MB的文件时非常缓慢,而file()则快得多。

只需尝试每次读取100行并创建批量插入,fopen()需要37秒,而file()只需要4秒。这一定是由于file()内置了string->array步骤。

我建议您尝试所有的文件处理选项,以确定哪种最适合您的应用程序。


-1

对于大文件,使用file_get_contents()要小心。虽然6兆字节应该没问题,但流式传输更好,因为它不会先将整个文件读入内存。 - Dustin Graham

-1
如果问题是由于内存限制引起的,您可以尝试将其设置为更高的值(这取决于PHP的配置,可能有效也可能无效)。
此代码将内存限制设置为12 Mb。
ini\_set("memory_limit","12M");

3
注意:虽然这可能有所帮助,但它只是拖延问题的时间:一旦出现一个15 MB的文件,问题就会重新出现。(如果您的文件永远不会超过某个限制,这可能会解决问题。) - Piskvor left the building

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接