老旧话题:PHP读取超大文件

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

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

“面试造火箭,入职拧螺丝”。然而,刚进来就拧螺丝的人若是可以对“PHP读取一个10G的超大文件”有所看法的话,“造火箭”也是早晚的事儿。当前为了可以来这里“拧螺丝”,仍是得先搞定“读取10G文件”这个问题。面试

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

... ...数组

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

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

首先,咱们尝试用php的file函数来做一把死,大家感觉一下:spa

<?php
$begin = microtime( true );
file( './test.log' );
$end = microtime( true );
echo "cost : ".( $end - $begin ).PHP_EOL;

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

这句英文的大概意思就是“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

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

相关文章
相关标签/搜索