PHP 如何读取一亿行的大文件
我们可能在很多场景下需要用 PHP 读取大文件,之后进行处理,如果你没有相关的经验可以看下,希望能给你带来一些启发。
模拟场景
我们有一个 1亿 行,大小大概为 3G 的日志文件,需要分析每一行获取一个 ID,然后拿这些 ID 逐行向数据库发起查询。
先想想 ...
遇到此类的问题稍微有点经验的程序员就需要考虑如下一些问题:
由于 PHP 可以利用的内存有限,即使可以修改我们也不要随便更改这个配置,就用默认的好了,由此可以确定这里肯定不能一次读完,需要考虑逐行分块读取
使用什么方法读比较合理?
思路
• 读写一个文件通常处理的流程是【打开】、【写入|读取】、【关闭】
• 我们知道 PHP 读取文件支持curl、file_get_contents、fopen,前两个对请求远程文件支持的比较好,可以一次性把结果读取到一个字符串里,里面封装了 http 请求以及读写文件完整流程的一些方法,使得我们读取文件非常方便,一个函数搞定,而另外一个fopen就把所有操作权都给你了,你需要怎么读怎么写自己组合
• 拿到 ID 后需要控制对数据库查询的次数,每行分析出 ID 就查数据库也是不合理的,如果考虑把所有 ID 拿到,整体一次请求数据库也是不现实的,PHP 变量可以控制的内存有限,而且数据库也不可能让你一次查询 3G 的 SQL 啊
我们不需要读取每行数据都执行打开、关闭文件,这种开销是巨大的,基于以上考虑,只有 fopen 可选了,fopen 可以分步骤进行操作,对于向数据库查询的操作,我们应该尽量控制单次请求 SQL 的 ID 数量在一个合理的范围,所以脑子里大概有这么个东西
<?php // 1、打开日志文件 // 2、循环读取每一行,保持打开状态 while { // 2.1、获取每一行数据,分析出我们需要的 ID // 2.2、拿着 ID 去数据库进行查询 # 由于是连接到数据库,我们也需要考虑操作数据库的连接、断开等开销成本 # 这里应该实现每查询一批 ID,批次的向数据库进行查询,以确保减少这部分的开销 } // 3、关闭日志文件
开始动手了
模拟机器配置
对于读写来说机器的配置及运行环境很重要,下面介绍下我的试验环境
CPU:4核 Intel(R) Core(TM) i7-4750HQ CPU @ 2.00GHz内存:2G硬盘:PCI-E SSD
PHP INFO
$ php -vPHP 7.1.8 (cli) (built: Aug 4 2017 18:59:36) ( NTS )Copyright (c) 1997-2017 The PHP GroupZend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies$ php -i | grep memory_limitmemory_limit => 128M => 128M
测试写入
我们使用以下脚本 write.php 写入到 test.log 中
<?php$start = date("Y-m-d H:i:s");$line = 0; // 行数$count = 0; // 行计数器$str = ""; // 初始写入的字符串$file = fopen("./test.log", "w");while($line <= 100000000){ ++$line; ++$count; $str .= "The line number is " . $line . "
"; // 为了减少 fwrite 的开销,我们每累计 200 万行写入一次 // 为啥是 200 万?试了 300 万就超出内存限制了,如果这个可以接受就这个了,不行再调整 if($count == 2000000){ fwrite($file, $str); // 重置行计数、字符串 $str = ""; $count = 0; }}echo "start:" . $start . ",end:" . date("Y-m-d H:i:s") . "
";
执行,验证是不是写的没问题
// 执行写入脚本$ php write.phpstart:2017-10-30 00:24:40,end:2017-10-30 00:26:05// 查看日志大小,du -sh 是展示一个人类能看得懂的文件大小,灰常有用$ du -sh test.log2.7G test.log// 查看日志多少行$ wc -l test.log100000000 test.log// 查看日志前 5 行$ head -5 test.logThe line number is 1The line number is 2The line number is 3The line number is 4The line number is 5// 查看日志倒数 5 行$ tail -n 5 test.logThe line number is 99999996The line number is 99999997The line number is 99999998The line number is 99999999The line number is 100000000
可以看到写入大概执行了一分半左右,这个速度还可以接受吧,接受不了你自己调整那个 200 万
测试读取
我们的读取脚本 read.php 如下
<?php$start = date('Y-m-d H:i:s');$count = 0; // 计数器,累计到一定值重置$ids = ""; // ID 集合// 打开文件$file = fopen('./test.log', 'r');while(!feof($file)){ // 获取当前指针行的数据,stream_get_line 的作用和 fgets 类似,不过这个可以指定换行符 $current = stream_get_line($file, 1024, "
"); if($current){ ++$count; // 拼接需要查询的 ID,假设你要获取的是最后一个空格之后的值 $ids .= "," . substr($current, strrpos($current, " ")+1); // 为啥这里是 1000 ?多了不更快么? // 是的,对于向数据库发起查询操作拼接 sql 来说 in 的 id 数量多少也很重要,我这边实验的是 1000 个 id 查询一次,并没有考虑太多 // 如果实际用到这种还需要结合 sql 的 max_allowed_packet 更多的考量这里的值怎么样是比较合理的 if($count == 1000){ $sql = "select id from table where id in(" . ltrim($ids, ",") . ")"; // 查数据库 $count = 0; $ids = ""; } }}// 关闭文件fclose($file);echo 'start:' . $start . ',end:' . date('Y-m-d H:i:s') . "
";
执行
$ php read.phpstart:2017-10-30 00:30:18,end:2017-10-30 00:30:49
实验了多次基本读取时间维持在半分钟左右,这个速度我觉得还行 ...
总结
以我的配置来说,对于 3G 的文件写入时间大概在一分半钟左右,读取时间在半分钟左右,也就是说类似的需求在如今 SSD 这么便宜的情况下做一些大文件读取的任务,PHP 读取的过程不会受限太多,随着数据量的增大,while 里面的每一个函数、每一个方法的使用都显得尤为重要,举例来说,在做实验的过程中用到了 echo 函数,如果去掉就会减少很多倍的时间。