首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >使用PHP从文件中读取最后一行(即“尾”)的最佳方式是什么?

使用PHP从文件中读取最后一行(即“尾”)的最佳方式是什么?
EN

Stack Overflow用户
提问于 2013-02-22 21:59:03
回答 7查看 52.3K关注 0票数 80

在我的PHP应用程序中,我需要从许多文件(主要是日志)的末尾开始读取多行。有时我只需要最后一个,有时我需要几十个或几百个。基本上,我想要像Unix tail命令一样灵活的命令。

这里有关于如何从文件中获取最后一行的问题(但我需要N行),并给出了不同的解决方案。我不确定哪一个是最好的,哪一个表现更好。

EN

回答 7

Stack Overflow用户

回答已采纳

发布于 2013-02-22 21:59:03

方法概述

在互联网上搜索,我发现了不同的解决方案。我可以将它们分成三种方法:

使用 PHP的function;

  • cheating用户,在系统上运行tail命令的file()
  • 用户;使用fseek().

在打开的文件中愉快跳转的

  • 强大用户

我最终选择(或写下)了五个解决方案,一个是幼稚的,一个是欺骗的,还有三个强大的。

  1. 最简洁的 ,使用内置的数组functions.
  2. The ,它有一个小问题:如果tail不可用,它就不能运行,即在非Unix (Windows)或不允许系统functions.
  3. The的受限环境中解决方案从文件末尾读取single bytes搜索(和计数)新行字符,found .
  4. The 多字节缓冲解决方案优化了大文件,发现缓冲区长度是动态变化的.
  5. A略微,根据要检索的行数决定。

All solutions work。从某种意义上说,它们从任何文件和我们请求的任意行数返回预期的结果(除了解决方案#1,它可以在大文件的情况下打破PHP内存限制,不返回任何内容)。但是哪一个更好呢?

性能测试

为了回答这个问题,我运行了测试。这就是这些事情是怎么做的,不是吗?

我准备了一个样例100KB文件,将在我的/var/log目录中找到的不同文件组合在一起。然后,我编写了一个PHP脚本,该脚本使用五个解决方案中的每一个来检索文件末尾的1、2、..、10、20、...、100、200、...、1000行。每个测试重复10次(大约相当于5×28×10= 1400个测试),以微秒为单位测量平均运行时间

我使用GHz命令行解释器在我的本地开发机器(xubuntu12.04,PHP5.3.10,2.70php双核CPU,2GBRAM)上运行该脚本。结果如下:

解决方案#1和#2似乎是最糟糕的。只有当我们需要阅读几行内容时,解决方案#3才是好的。注意动态缓冲区大小是如何优化算法的:几行代码的执行时间更短,因为减少了缓冲区。

让我们尝试一个更大的文件。如果我们必须读取10 MB日志文件,该怎么办?

到目前为止,解决方案1是最糟糕的:实际上,将整个10MB文件加载到内存中并不是一个好主意。我还在1MB和100MB的文件上运行了测试,实际上是相同的情况。

对于微小的日志文件呢?这是一个10 KB文件的图表:

解决方案#1是现在最好的方案!对于PHP来说,将10KB加载到内存中并不是什么大问题。#4和#5的表现也很好。然而,这是一种边缘情况:10KB日志意味着大约150/200行……

你可以下载我所有的测试文件,源代码和结果。

最后的想法

对于一般的用例,强烈推荐使用:适用于任何文件大小,并且在读取几行代码时表现得特别好。

如果要读取大于10 KB的文件,请避免使用。

对于我运行的每个测试,解决方案和都不是最好的:#2永远不会在少于2ms的时间内运行,#3受您询问的行数的影响很大(只有1到2行才能很好地工作)。

票数 278
EN

Stack Overflow用户

发布于 2017-04-26 20:27:47

这是一个修改过的版本,也可以跳过最后一行:

/**
 * Modified version of http://www.geekality.net/2011/05/28/php-tail-tackling-large-files/ and of https://gist.github.com/lorenzos/1711e81a9162320fde20
 * @author Kinga the Witch (Trans-dating.com), Torleif Berger, Lorenzo Stanco
 * @link http://stackoverflow.com/a/15025877/995958
 * @license http://creativecommons.org/licenses/by/3.0/
 */    
function tailWithSkip($filepath, $lines = 1, $skip = 0, $adaptive = true)
{
  // Open file
  $f = @fopen($filepath, "rb");
  if (@flock($f, LOCK_SH) === false) return false;
  if ($f === false) return false;

  if (!$adaptive) $buffer = 4096;
  else {
    // Sets buffer size, according to the number of lines to retrieve.
    // This gives a performance boost when reading a few lines from the file.
    $max=max($lines, $skip);
    $buffer = ($max < 2 ? 64 : ($max < 10 ? 512 : 4096));
  }

  // Jump to last character
  fseek($f, -1, SEEK_END);

  // Read it and adjust line number if necessary
  // (Otherwise the result would be wrong if file doesn't end with a blank line)
  if (fread($f, 1) == "\n") {
    if ($skip > 0) { $skip++; $lines--; }
  } else {
    $lines--;
  }

  // Start reading
  $output = '';
  $chunk = '';
  // While we would like more
  while (ftell($f) > 0 && $lines >= 0) {
    // Figure out how far back we should jump
    $seek = min(ftell($f), $buffer);

    // Do the jump (backwards, relative to where we are)
    fseek($f, -$seek, SEEK_CUR);

    // Read a chunk
    $chunk = fread($f, $seek);

    // Calculate chunk parameters
    $count = substr_count($chunk, "\n");
    $strlen = mb_strlen($chunk, '8bit');

    // Move the file pointer
    fseek($f, -$strlen, SEEK_CUR);

    if ($skip > 0) { // There are some lines to skip
      if ($skip > $count) { $skip -= $count; $chunk=''; } // Chunk contains less new line symbols than
      else {
        $pos = 0;

        while ($skip > 0) {
          if ($pos > 0) $offset = $pos - $strlen - 1; // Calculate the offset - NEGATIVE position of last new line symbol
          else $offset=0; // First search (without offset)

          $pos = strrpos($chunk, "\n", $offset); // Search for last (including offset) new line symbol

          if ($pos !== false) $skip--; // Found new line symbol - skip the line
          else break; // "else break;" - Protection against infinite loop (just in case)
        }
        $chunk=substr($chunk, 0, $pos); // Truncated chunk
        $count=substr_count($chunk, "\n"); // Count new line symbols in truncated chunk
      }
    }

    if (strlen($chunk) > 0) {
      // Add chunk to the output
      $output = $chunk . $output;
      // Decrease our line counter
      $lines -= $count;
    }
  }

  // While we have too many lines
  // (Because of buffer size we might have read too many)
  while ($lines++ < 0) {
    // Find first newline and remove all text before that
    $output = substr($output, strpos($output, "\n") + 1);
  }

  // Close file and return
  @flock($f, LOCK_UN);
  fclose($f);
  return trim($output);
}
票数 6
EN

Stack Overflow用户

发布于 2017-01-13 20:16:09

这也是可行的:

$file = new SplFileObject("/path/to/file");
$file->seek(PHP_INT_MAX); // cheap trick to seek to EoF
$total_lines = $file->key(); // last line number

// output the last twenty lines
$reader = new LimitIterator($file, $total_lines - 20);
foreach ($reader as $line) {
    echo $line; // includes newlines
}

或者不使用LimitIterator

$file = new SplFileObject($filepath);
$file->seek(PHP_INT_MAX);
$total_lines = $file->key();
$file->seek($total_lines - 20);
while (!$file->eof()) {
    echo $file->current();
    $file->next();
}

不幸的是,您的测试用例在我的机器上出现了分段错误,因此我无法判断它的执行情况。

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/15025875

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档