PHP中读取文件最后几行的最佳方法是什么?

lor*_*o-s 69 php performance logging

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

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

lor*_*o-s 245

方法概述

在互联网上搜索,我遇到了不同的解决方案.我可以用三种方法对它们进行分组:

  • 使用file()PHP功能的天真的;
  • 欺骗那些tail在系统上运行命令的人;
  • 强大的,快乐地跳过打开的文件使用fseek().

我最终选择(或写)五个解决方案,一个天真的,一个作弊的和三个强大的解决方案.

  1. 最简洁的天真解决方案,使用内置的数组函数.
  2. 基于唯一可能的解决tail命令,它有一点点大的问题:如果它不跑tail不可用,因为在非Unix(Windows)或在限制环境不允许的系统功能.
  3. 解决方案,从文件末尾读取单个字节,搜索(和计算)新行字符,在此处找到.
  4. 这里提供了针对大文件优化的多字节缓冲解决方案 .
  5. 解决方案#4的略微修改版本,其中缓冲区长度是动态的,根据要检索的行数决定.

所有方案都有效.从某种意义上说,它们从任何文件和我们要求的任意数量的行返回预期的结果(解决方案#1除外,在大文件的情况下可以破坏PHP内存限制,不返回任何内容).但哪一个更好?

性能测试

要回答这个问题,我会进行测试.这就是这些事情的完成方式,不是吗?

我准备了一个100 KB的样本文件,将我/var/log目录中的不同文件连接在一起.然后我写了一个PHP脚本,它使用五个解决方案中的每一个来从文件末尾检索1,2,...,10,20,... 100,200,...,1000行.每个单独的测试重复十次(这类似于5×28×10 = 1400次测试),测量平均经过的时间(以微秒为单位).

我使用PHP命令行解释器在我的本地开发机器(Xubuntu 12.04,PHP 5.3.10,2.70 GHz双核CPU,2 GB RAM)上运行脚本.结果如下:

样本100 KB日志文件的执行时间

解决方案#1和#2似乎是更糟糕的.只有当我们需要阅读几行时,解决方案#3才是好的.解决方案#4和#5似乎是最好的解决方案. 请注意动态缓冲区大小如何优化算法:由于缓冲区减少,几行的执行时间略小.

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

样本10 MB日志文件的执行时间

现在解决方案#1是最糟糕的一个:实际上,将整个10 MB文件加载到内存中并不是一个好主意.我也在1MB和100MB文件上运行测试,这几乎是相同的情况.

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

样本10 KB日志文件的执行时间

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

您可以在此处下载我的所有测试文件,来源和结果 .

最后的想法

对于一般用例,强烈建议使用解决方案#5:对于每个文件大小都能很好地工作,并且在读取几行时表现特别好.

如果您应该读取大于10 KB的文件,请避免使用解决方案#1.

对于我运行的每个测试,解决方案#2#3都不是最好的解决方案:#2永远不会在不到2ms的时间内运行,而#3很大程度上受到你要求的行数的影响(只有1或2行才能很好地工作) ).

  • 可能是我见过的最好的答案之一.选项,多重测试,结论.你需要一枚奖牌. (6认同)
  • @Svish代码在GitHub Gist上.如果你在谈论整个测试文件,我认为没有必要把它们放在一个回购中......关于优化:我真的很想专注于性能,因为我必须非常强烈地使用那些代码来读取少数行(小于10).所以,我似乎没有必要使用大缓冲区.请注意,轴是对数的:对于少数行,减少的缓冲区意味着执行时间的一半! (5认同)

Kin*_*tch 5

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

/**
 * 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);
}
Run Code Online (Sandbox Code Playgroud)