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

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

前言

我写这篇笔记的原因是现在网络上关于 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 去测一下代码的占用内存和运行时间。

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

(0)

相关推荐

  • php遍历目录与文件夹的多种方法详解

    遍历目录或遍历目录下指定类型的文件,这是每一个童鞋在写程序的时候难免会用到的.PHP本身也提供了很多灰常有用的函数,正确地使用它们,不会有错滴.下面就我个人学习过程中的一些总结,希望对想学PHP的童鞋有所帮助.本函数可以列出指定目录下所有的文件(包括子目录下的) 复制代码 代码如下: function getfiles($path){ foreach(scandir($path) as $afile){if($afile=='.'||$afile=='..') continue; if(is_d

  • PHP 远程文件管理,可以给表格排序,遍历目录,时间排序

    复制代码 代码如下: <?php $rootdir="./"; $spacenum=0; $filenum=0; $allfilesize=0; echo "<h1>文件管理调试测试</h1>"; echo "<a href=''>重置</a>"; readLogDir($rootdir); echo "<hr>"; echo "Total files

  • php 遍历显示文件夹下所有目录、所有文件的函数,没有分页的代码

    <pre> <?php /********************** 一个简单的目录递归函数 第一种实现办法:用dir返回对象 ***********************/ function tree($directory) { $mydir=dir($directory); echo "<ul>\n"; while($file=$mydir->read()){ if((is_dir("$directory/$file"))

  • 用PHP读取超大文件的实例代码

    去年年底的各种网站帐号信息的数据库泄漏,很是给力啊,趁机也下载了几个数据库,准备学学数据分析家来分析一下这些帐号信息.虽然这些数据信息都已经被"整理"过的,不过自己拿来学习也挺有用的,毕竟有这么大的数据量. 数据量大带来的问题就是单个文件很大,能够打开这个文件相当不容易,记事本就不要指望了,果断死机.用MSSQL的客户端也打不开这么大的SQL文件,直接报内存不足,原因据说是MSSQL在读取数据的时候,是一次性地将读取到的数据放在内存中,如果数据量过大,而内存不足,则会直接导致系统瘫掉.

  • PHP遍历某个目录下的所有文件和子文件夹的实现代码

    复制代码 代码如下: <?php function read_all_dir ( $dir )    {        $result = array();        $handle = opendir($dir);        if ( $handle )        {            while ( ( $file = readdir ( $handle ) ) !== false )            {                if ( $file != '.'

  • PHP遍历目录文件的常用方法小结

    本文实例总结了PHP遍历目录文件的常用方法.分享给大家供大家参考,具体如下: 测试算法(源代码经过本站工具http://tools.jb51.net/code/jb51_php_format进行格式化处理,以便于读者阅读) 算法1.简短系 foreach(glob('*.*') as $filename) { echo 'Filename:'.$filename.; } 算法2.规矩系 if($handle = opendir('C:\\Inetpub\\wwwroot\\test\\')){

  • php实现读取超大文件的方法

    通常来说在php读取大文件的时候,我们采用的方法一般是一行行来讲取,而不是一次性把文件全部写入内存中,这样会导致php程序卡死,下面就给大家介绍这样一个例子. 读取大文件最后几行数据: <?php /** * 取文件最后$n行 * * @param string $filename 文件路径 * @param int $n 最后几行 * @return mixed false表示有错误,成功则返回字符串 */ function FileLastLines($filename, $n){ if(!

  • PHP目录与文件操作技巧总结(创建,删除,遍历,读写,修改等)

    本文实例总结了PHP目录与文件操作技巧.分享给大家供大家参考,具体如下: Demo1.php <?php //将一个路径赋给一个变量 //它目前来说,只是一个字符串,字符串表示的是一个目录的路径 //文件名包含,文件的名称 + 文件的扩展名(就是.后面的文件类型) //文件的扩展名说白了就是文件后缀 $path = 'C:\AppServ\www\Basic6\Demo1.php'; // echo basename($path); // echo '<br/>'; // //dirna

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

    这不是一篇教程,这是一篇笔记,所以我不会很系统地论述原理和实现,只简单说明和举例. 前言 我写这篇笔记的原因是现在网络上关于 PHP 遍历目录文件和 PHP 读取文本文件的教程和示例代码都是极其低效的,低效就算了,有的甚至好意思说是高效,实在辣眼睛. 这篇笔记主要解决这么几个问题: PHP 如何使用超低内存快速遍历数以万计的目录文件? PHP 如何使用超低内存快速读取几百MB甚至是GB级文件? 顺便解决哪天我忘了可以通过搜索引擎搜到我自己写的笔记来看看.(因为需要 PHP 写这两个功能的情况真的

  • python 遍历目录(包括子目录)下所有文件的实例

    如下所示: def list_all_files(rootdir): import os _files = [] list = os.listdir(rootdir) #列出文件夹下所有的目录与文件 for i in range(0,len(list)): path = os.path.join(rootdir,list[i]) if os.path.isdir(path): _files.extend(list_all_files(path)) if os.path.isfile(path):

  • java从文件中读取数据的六种方法

    目录 1.Scanner 2.Files.lines (Java 8) 3.Files.readAllLines(java8) 4.Files.readString(JDK 11) 5.Files.readAllBytes() 6.经典管道流的方式 本文主要介绍了java从文件中读取数据的六种方法,分享给大家,具体如下: Scanner(Java 1.5) 按行读数据及String.Int类型等按分隔符读数据. Files.lines, 返回Stream(Java 8) 流式数据处理,按行读取

  • Python多进程分块读取超大文件的方法

    本文实例讲述了Python多进程分块读取超大文件的方法.分享给大家供大家参考,具体如下: 读取超大的文本文件,使用多进程分块读取,将每一块单独输出成文件 # -*- coding: GBK -*- import urlparse import datetime import os from multiprocessing import Process,Queue,Array,RLock """ 多进程分块读取文件 """ WORKERS = 4

  • Python3实现从文件中读取指定行的方法

    本文实例讲述了Python3实现从文件中读取指定行的方法.分享给大家供大家参考.具体实现方法如下: # Python的标准库linecache模块非常适合这个任务 import linecache the_line = linecache.getline('d:/FreakOut.cpp', 222) print (the_line) # linecache读取并缓存文件中所有的文本, # 若文件很大,而只读一行,则效率低下. # 可显示使用循环, 注意enumerate从0开始计数,而line

  • C#从文件流读取xml文件到DataSet并显示的方法

    本文实例讲述了C#从文件流读取xml文件到DataSet并显示的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: DataSet ds= new DataSet (); FileStream fs = new FileStream (Server.MapPath=("somexml.xml"),FileMode.Open,FileAccess.Read); ds.ReadXml (fs); DataGrid1.DataSource = ds; DataGrid1.D

  • java使用多线程读取超大文件

    接上次写的"JAVA读取超大文件".在读取超过10G的文件时会发现一次读一行的速度实在是不能接受,想到使用多线程+FileChannel来做一个使用多线程版本. 基本思路如下: 1.计算出文件总大小 2.分段处理,计算出每个线程读取文件的开始与结束位置 (文件大小/线程数)*N,N是指第几个线程,这样能得到每个线程在读该文件的大概起始位置 使用"大概起始位置",作为读文件的开始偏移量(fileChannel.position("大概起始位置"))

  • Python基于read(size)方法读取超大文件

    pyhon读取文件很方便,但是,如果文件很大,而且还是一行文件,那就蛋疼了. 不过还好有read(size)方法,这个方法就是每次读取size大小的数据到内存中 下面来个示例 def readlines(f, separator): ''' 读取大文件方法 :param f: 文件句柄 :param separator: 每一行的分隔符 :return: ''' buf = '' while True: while separator in buf: position = buf.index(s

随机推荐