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

38次阅读

共计 3479 个字符,预计需要花费 9 分钟才能阅读完成。

作为一名常年深耕 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
保存文件并运行,结果如下图所示:

正文完
 0