当前位置: 首页 > 后端技术 > PHP

PHP超低内存遍历目录文件读取超大文件

时间:2023-03-29 17:14:50 PHP

本文不是教程,只是笔记,所以不系统地讨论原理和实现,只是简单的讲解和举例。前言之所以写这篇笔记是因为网上关于PHP遍历目录文件和PHP读取文本文件的教程和示例代码效率极低。眼睛。本篇笔记主要解决几个问题:PHP如何利用超低内存快速遍历上万个目录文件?PHP如何利用超低内存快速读取数百MB甚至GB级别的文件?顺便说一句,有一天我忘记通过搜索引擎找到我自己写的笔记。(因为需要PHP写这两个函数的情况真的很少,我记性不好,以免忘记再走弯路)遍历目录文件这个方法网上的示例代码大多是glob或者opendir+readdir组合。目录下的文件不多还好,文件多了就会出问题(这里指的是打包成函数统一返回数组的时候),太大的数组会需要使用大内存,不仅会导致速度变慢,内存不足时直接死机。这时候正确的实现方式是使用yield关键字返回。以下是我最近使用的代码:valid()){yield$sub->current();$子->下一个();}if($include_dirs)yield$rfile;}else{yield$rfile;}}closedir($dh);}}//使用$glob=glob2foreach('/var/www');while($glob->valid()){//当前文件$filename=$glob->current();//这是包含路径的完整文件名//echo$filename;//指向下一个,同样$glob->next();}yield返回生成器对象(不知道的可以先去了解一下PHP的生成器),并且数组不是立即生成的,所以目录里不管有多少文件,都不会有巨型数组,内存消耗大低到几十kb级别可以忽略不计,时间消耗几乎只有循环消耗。读取文本文件类似于遍历目录文件。网上教程基本都是使用file_get_contents读入内存或者fopen+feof+fgetc组合读入即用。小文件没有问题,但是处理大文件时,就会出现内存不足等问题。使用file_get_contents读取数百MB的文件几乎是自杀。正确处理这个问题的方法也和yield关键字有关,通过yield逐行处理,或者SplFileObject从指定位置读取。逐行读取整个文件:valid()){//当前行文本$line=$glob->current();//逐行处理数据//$line//指向下一个,不能少$glob->next();}通过yield逐行读取文件,使用多少内存取决于每个有多大一行中的数据量?如果是每行只有几百字节的日志文件,即使文件超过100M,占用的内存也只有KB级别。但是很多时候我们并不需要一次读取整个文件。比如我们要分页读取一个1G的日志文件,我们可能希望在第一页读取前1000行,在第二页读取前1000行。2000行,此时不能使用上面的方法,因为该方法虽然占用内存低,但是循环上万次还是需要时间的。这时候改用SplFileObject,SplFileObject可以从指定的行数开始读取。下面的例子是写入数组并返回。大家可以根据自己的业务决定写不写数组。我懒得改了。seek($offset);$i=0;while(!$fp->eof()){//必须放在开头$i++;//只读取$count这么多行if($i>$count)break;$line=$fp->current();$line=trim($line);$arr[]=$行;//指向下一个,不下$fp->next();}return$arr;}上面说的都是文件很大但是每行数据量很少的情况,有时候不是这样,有时候一行也有几百MB的数据,所以如何处理?如果是这种情况,则取决于具体业务。SplFileObject可以通过fseek定位到字符位置(注意和seek定位的行数不同),然后通过fread读取指定长度的字符。也就是说,通过fseek和fread,可以分段读取一个超长的字符串,也就是可以做到超低内存处理,但是具体怎么做,要看具体的业务需求让你做什么做。复制大文件顺便说一句,PHP复制文件。使用复制功能复制小文件是没有问题的。复制大文件最好使用数据流。示例如下:next();code,表示指向下一项。这个很重要,因为如果没有,下一个循环就会得到这一次的结果。例如:一个文本文件中有三行文本,分别是111111、222222、333333。当我们使用下面的代码读取时,while会循环三次,$line每次对应111111、222222、333333。valid()){//当前行文本$line=$glob->current();//逐行处理数据//$line//指向下一条,不下$glob->next();}但是,如果没有$glob->next();line,$line会一直读到第一行111111,会造成死循环或者读到意外的数据。看到这里,你可能会觉得这是废话,不不不,这个错误在理论上是不容易出现的,但是在实际编程中,我们可能会使用继续跳转到下一个循环。如果写了记不住,在$glob->next();之前使用continue跳到下一个循环,下一个循环的$line仍然是这次的值,导致异常甚至死循环。解决这个问题,除了保持编码警惕外,还可以修改$glob->next();的位置。valid()){//当前项目$line=$glob->current();//指向下一个,不能少$glob->next();//注意,此时已经指向了下一项//然后使用$glob->current()获取$line的值,而不是$line的值下一项的值//这之后,可以放心使用continue//但是不要忘记只通过$line读取当前项//逐行处理数据}这个坑我踩过,无意间使用continue会导致读取数据是不正确的。其实这种错误导致死循环程序崩溃是件好事。立即调查就可以知道结果。最要命的是只读取了错误的数据,一时半会儿人们都察觉不到。另外,增加修改大文件的要点。读取大文件通常涉及修改它。如果是要从中提取数据或者对其进行大幅度修改,我们可以使用fopen+fwrite的组合,配合生成器对象逐行处理数据,然后逐行写入。这也是非常有效的。尽可能避免将它们存储在变量中,然后将它们一起写入以避免内存爆炸。valid()){//当前行文本$line=$glob->current();//逐行处理数据//将处理后的数据写入新文件fwrite($handle,$line."\n");//指向下一个,$glob->next();}fclose($handle);如果是修改大文件中的小细节,我还没有这样做过,不过据我所知,好像是通过StreamFunctions的filter来实现的,效率比较高。