PHP读取超大文件

  • 原创
  • 发布时间: 1个月前
  • 收藏数: 0 / 点赞数: 0 / 阅读数: 27

作为一名常年深耕curd的PHPer,关注内存那是不可能的,反正apache或者fpm都帮我们做了,况且运行一次就销毁,根本就不存在什么内存问题。

然而偏偏就有些个不开眼的人把这些个东西当面试题,比如总有刁民用“php读取一个10G的超大文件”当面试题来问你。当然了,作为一个和我一样的普普通通的蠢货,你听到这个问题的第一瞬间是懵逼,第二瞬间是卧槽,第三瞬间是保持结巴状态。

“面试造火箭,入职拧螺丝”。

然而,刚进来就拧螺丝的人如果能够对“PHP读取一个10G的超大文件”有所见解的话,“造火箭”也是迟早的事儿。

当前为了能够来这里“拧螺丝”,还是得先搞定“读取10G文件”这个问题。

要想读取10G的文件,首先,你得有个10G的文件… …

其实,相对来说也是比较简单的事情,我们随便找一个nginx的日志文件,哪怕只有10KB,假设文件名是test.log,然后呢执行” cat test.log >> test.log “,听我说少年,30秒左右你就该按下ctrl + C了,比如我这里,你们感受一下:

202MB,作为实验演示,够意思了。难不成真要造10G的文件?

首先,我们尝试用php的file函数来作一把死,你们感受一下:

<?php

$begin = microtime( true );

file( './test.log' );

$end = microtime( true );

echo "cost : ".( $end - $begin ).PHP_EOL;

保存为test.php,然后命令行下执行一把,结果如下图所示:

这句英文的大概意思就是“PHP最大只给每个进程分配了128MB内存,然而你特么张口要202MB?”所以,我们修改一下php配置文件… …

千万不要手软,把这个参数改成1024MB,然后再次执行上面的php脚本:

然后,我们再试试最爱的file_get_contents()函数,结果如下图:

文件已经一次性全部被载入到内存中并将文件的每一行保存到了一个php数组中,我的机器是10G内存+256G固态硬盘,一次性载入这个202MB的文件file函数用了0.67秒钟、file_get_contents函数用了0.25秒钟(看起来file_get_content要比file靠谱的多),不过,敲重点的我们调整了配置文件才可以读取202MB的文件,如果摆在我们面前的是一个100G的文件呢?或者说,系统提供的php配置最多之给20MB内存而你又无法修改呢?

我们重点是如何在内存有限的机器上读取体积几百倍于内存的文件。下面,我们把memory_limit调整成16M,开启困难模式。

202MB的文件,允许被分配的内存为16MB,所以,总体思路其实也很简单,就是一点儿一点儿地读,只要每次读取的内容小于16MB,那就一定不会有问题,首先我们感受一下一个字符一个字符读,出场嘉宾是fgetc函数:

<?php

$begin = microtime( true );

$fp = fopen( './test.log' );

while( false !== ( $ch = fgetc( $fp ) ) ){

// ⚠️⚠️⚠️ 作为测试代码是否正确,你可以打开注释 ⚠️⚠️⚠️
// 但是,打开注释后屏显字符会严重拖慢程序速度!也就是说程序运行速度可能远远超出屏幕显示速度
//echo $char.PHP_EOL;

}

fclose( $fp );

$end = microtime( true );

echo "cost : ".( $end - $begin ).PHP_EOL;

运行结果如下图:

虽然只有给了16M内存,但我们还是成功将202M文件全部读出来了,只不过这个运行速度是差了那么点儿意思,不大行。不能一个字母一个字母地读,这次我们一行一行地读:

<?php

$begin = microtime( true );

$fp = fopen( './test.log', 'r' );

while( false !== ( $buffer = fgets( $fp, 4096 ) ) ){
  //echo $buffer.PHP_EOL;
}

if( !feof( $fp ) ){
  throw new Exception('... ...');
}

fclose( $fp );

$end = microtime( true );

echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;

运行结果如下图:

一行一行果然比一个一个字符要快很多,转念一想吧,系统分配给我们的内存上限是16MB,那我们索性一次读取一定量容量数据看看,会不会更快:

<?php

$begin = microtime( true );

$fp = fopen( './test.log', 'r' );

while( !feof( $fp ) ){
  // 如果你要使用echo,那么,你会很惨烈...
  fread( $fp, 10240 );
}

fclose( $fp );

$end = microtime( true );

echo "cost : ".( $end - $begin ).' sec'.PHP_EOL;

exit;

保存代码,运行一把,屌了屌了!!!在内存有限的情况下,我们还把时间缩短到了0.1秒!

然后我们考虑将问题升级一下,依然是上述这个202M的文件,这次我们要求读取倒数后5行的内容,这个问题看起来屌了些许,用原来的fread啥的虽然奏效但总感觉比较愚蠢。

所以,现在又得引入全新的函数来解决这个问题:ftell和fseek。

其中,ftell用于告知当前文件读取指针所在位置,fseek可以手动设定文件读取指针的位置。

我建议大家去手册上重点观摩一下fseek函数:点击这里

<?php

$fp = fopen( './test1.log', 'r' );

$line = 5;

$pos = -2;

$ch = '';

$content = '';

while( $line > 0 ){
  while( $ch != "\n" ){
    fseek( $fp, $pos, SEEK_END );
    $ch = fgetc( $fp );
    $pos--;
  }
  $ch = '';
  $content .= fgets( $fp );
  $line--;
}

echo $content;

exit;

其中test1.log文件的内容如下:

aa
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccccccccccccc
dddddddddddddddddddddddddddddddd
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ffffffffffffffffffffffffffffffff
1111111111
2222222222

保存文件并运行,结果如下图所示:

评论