PHP超低内存遍历目录文件和读取超大文件的方法


Posted in PHP onMay 01, 2019

这不是一篇教程,这是一篇笔记,所以我不会很系统地论述原理和实现,只简单说明和举例。

前言

我写这篇笔记的原因是现在网络上关于 PHP 遍历目录文件和 PHP 读取文本文件的教程和示例代码都是极其低效的,低效就算了,有的甚至好意思说是高效,实在辣眼睛。

这篇笔记主要解决这么几个问题:

PHP 如何使用超低内存快速遍历数以万计的目录文件?

PHP 如何使用超低内存快速读取几百MB甚至是GB级文件?

顺便解决哪天我忘了可以通过搜索引擎搜到我自己写的笔记来看看。(因为需要 PHP 写这两个功能的情况真的很少,我记性不好,免得忘了又重走一遍弯路)

遍历目录文件

网上关于这个方法的实现大多示例代码是 glob 或者 opendir + readdir 组合,在目录文件不多的情况下是没问题的,但文件一多就有问题了(这里是指封装成函数统一返回一个数组的时候),过大的数组会要求使用超大内存,不仅导致速度慢,而且内存不足的时候直接就崩溃了。

这时候正确的实现方法是使用 yield 关键字返回,下面是我最近使用的代码:

<?php

function glob2foreach($path, $include_dirs=false) {
  $path = rtrim($path, '/*');
  if (is_readable($path)) {
    $dh = opendir($path);
    while (($file = readdir($dh)) !== false) {
      if (substr($file, 0, 1) == '.')
        continue;
      $rfile = "{$path}/{$file}";
      if (is_dir($rfile)) {
        $sub = glob2foreach($rfile, $include_dirs);
        while ($sub->valid()) {
          yield $sub->current();
          $sub->next();
        }
        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 从指定位置读取。

逐行读取整个文件:

<?php
function read_file($path) {
  if ($handle = fopen($path, 'r')) {
    while (! feof($handle)) {
      yield trim(fgets($handle));
    }
    fclose($handle);
  }
}
// 使用
$glob = read_file('/var/www/hello.txt');
while ($glob->valid()) {
  
  // 当前行文本
  $line = $glob->current();
  
  // 逐行处理数据
  // $line

  // 指向下一个,不能少
  $glob->next();
}

通过 yield 逐行读取文件,具体使用多少内存取决于每一行的数据量有多大,如果是每行只有几百字节的日志文件,即使这个文件超过100M,占用内存也只是KB级别。

但很多时候我们并不需要一次性读完整个文件,比如当我们想分页读取一个1G大小的日志文件的时候,可能想第一页读取前面1000行,第二页读取第1000行到2000行,这时候就不能用上面的方法了,因为那方法虽然占用内存低,但是数以万计的循环是需要消耗时间的。

这时候,就改用 SplFileObject 处理,SplFileObject 可以从指定行数开始读取。下面例子是写入数组返回,可以根据自己业务决定要不要写入数组,我懒得改了。

<?php

function read_file2arr($path, $count, $offset=0) {

  $arr = array();
  if (! is_readable($path))
    return $arr;

  $fp = new SplFileObject($path, 'r');
  
  // 定位到指定的行数开始读
  if ($offset)
    $fp->seek($offset); 

  $i = 0;
  
  while (! $fp->eof()) {
    
    // 必须放在开头
    $i++;
    
    // 只读 $count 这么多行
    if ($i > $count)
      break;
    
    $line = $fp->current();
    $line = trim($line);

    $arr[] = $line;

    // 指向下一个,不能少
    $fp->next();
  }
  
  return $arr;
}

以上所说的都是文件巨大但是每一行数据量都很小的情况,有时候情况不是这样,有时候是一行数据也有上百MB,那这该怎么处理呢?

如果是这种情况,那就要看具体业务了,SplFileObject 是可以通过 fseek 定位到字符位置(注意,跟 seek 定位到行数不一样),然后通过 fread 读取指定长度的字符。

也就是说通过 fseek 和 fread 是可以实现分段读取一个超长字符串的,也就是可以实现超低内存处理,但是具体要怎么做还是得看具体业务要求允许你怎么做。

复制大文件

顺便说下 PHP 复制文件,复制小文件用 copy 函数是没问题的,复制大文件的话还是用数据流好,例子如下:

<?php

function copy_file($path, $to_file) {

  if (! is_readable($path))
    return false;

  if(! is_dir(dirname($to_file)))
    @mkdir(dirname($to_file).'/', 0747, TRUE);
  
  if (
    ($handle1 = fopen($path, 'r')) 
    && ($handle2 = fopen($to_file, 'w'))
  ) {

    stream_copy_to_stream($handle1, $handle2);

    fclose($handle1);
    fclose($handle2);
  }
}

最后

我这只说结论,没有展示测试数据,可能难以服众,如果你持怀疑态度想求证,可以用 memory_get_peak_usage 和 microtime 去测一下代码的占用内存和运行时间。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
php 常用算法和时间复杂度
Jul 01 PHP
php登陆页的密码处理方式分享
Oct 14 PHP
php实现监听事件
Nov 06 PHP
php获取QQ头像并显示的方法
Dec 23 PHP
php根据一个给定范围和步进生成数组的方法
Jun 19 PHP
PHP实现批量修改文件后缀名的方法
Jul 30 PHP
PHP中对数组的一些常用的增、删、插操作函数总结
Nov 27 PHP
PHP判断表达式中括号是否匹配的简单实例
Oct 22 PHP
浅谈PHP的数据库接口和技术
Dec 09 PHP
php实现微信模拟登陆、获取用户列表及群发消息功能示例
Jun 28 PHP
PHP清除缓存的几种方法总结
Sep 12 PHP
PHP parse_ini_file函数的应用与扩展操作示例
Jan 07 PHP
Yii框架学习笔记之session与cookie简单操作示例
Apr 30 #PHP
YII框架学习笔记之命名空间、操作响应与视图操作示例
Apr 30 #PHP
YII框架关联查询操作示例
Apr 29 #PHP
YII框架页面缓存操作示例
Apr 29 #PHP
YII框架http缓存操作示例
Apr 29 #PHP
PHP常见的几种攻击方式实例小结
Apr 29 #PHP
php-fpm重启导致的程序执行中断问题详解
Apr 29 #PHP
You might like
PHP curl CURLOPT_RETURNTRANSFER参数的作用使用实例
2015/02/07 PHP
详细解读PHP的Yii框架中登陆功能的实现
2015/08/21 PHP
php利用ob_start()清除输出和选择性输出的方法
2018/01/18 PHP
PHP快速导出百万级数据到CSV或者EXCEL文件
2020/11/27 PHP
身份证号码前六位所代表的省,市,区, 以及地区编码下载
2007/04/12 Javascript
JSON 客户端和服务器端的格式转换
2009/08/27 Javascript
cookie中的path与domain属性详解
2013/12/18 Javascript
jQuery操作表格(table)的常用方法、技巧汇总
2014/04/12 Javascript
js 获取input点选按钮的值的方法
2014/04/14 Javascript
javascript中innerText和innerHTML属性用法实例分析
2015/05/13 Javascript
移动端JQ插件hammer使用详解
2015/07/03 Javascript
详解利用 Vue.js 实现前后端分离的RBAC角色权限管理
2017/09/15 Javascript
详解node.js中的npm和webpack配置方法
2018/01/21 Javascript
JavaScript面向对象的程序设计(犯迷糊的小羊)
2018/05/27 Javascript
vue实现动态按钮功能
2019/05/13 Javascript
[13:16]INFAMOUS vs VGJ T BO3
2018/06/07 DOTA
python算法学习之桶排序算法实例(分块排序)
2013/12/18 Python
python调用机器喇叭发出蜂鸣声(Beep)的方法
2015/03/23 Python
详解Python中dict与set的使用
2015/08/10 Python
Python装饰器基础详解
2016/03/09 Python
python机器人行走步数问题的解决
2018/01/29 Python
scrapy-redis的安装部署步骤讲解
2019/02/27 Python
浅析python标准库中的glob
2020/03/13 Python
Django Serializer HiddenField隐藏字段实例
2020/03/31 Python
Keras框架中的epoch、bacth、batch size、iteration使用介绍
2020/06/10 Python
KARATOV珠宝在线商店:俄罗斯珠宝品牌
2019/03/13 全球购物
Marlies Dekkers内衣法国官方网上商店:国际知名的荷兰内衣品牌
2019/03/18 全球购物
澳大利亚香水在线商店:City Perfume
2020/09/02 全球购物
将一个文本文件的内容按倒序打印出来
2015/01/05 面试题
火车来了教学反思
2014/02/11 职场文书
专科生就业求职信
2014/06/22 职场文书
向女朋友道歉的话
2015/01/20 职场文书
2016教师节问候语
2015/11/10 职场文书
2019员工保密协议书(3篇)
2019/09/23 职场文书
有关信念的名言语录集锦
2019/12/06 职场文书
spring cloud gateway中如何读取请求参数
2021/07/15 Java/Android