使用 PHP 从文件中读取最后几行(即“ tail”)的最佳方法是什么?

在我的 PHP 应用程序中,我需要 < strong > 读取从 许多文件 (大部分是日志) 基本上,我想要一些像 Unixtail一样灵活的东西 指挥官。

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

56235 次浏览

方法概述

在互联网上搜索,我找到了不同的解决方案。我可以把它们分组 有三种方法:

  • 使用 file() PHP 函数的;
  • 在系统上运行 tail命令的欺骗程序;
  • 强大的 使用 fseek()快乐地跳转打开的文件。

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

  1. 最简洁的 解决方案, 使用内置的数组函数。
  2. 基于 tail 命令的唯一可能的解决方案 一个小的大问题: 如果 tail不可用,它就不会运行 非 Unix (Windows)或在不允许系统的受限环境中 功能。
  3. 从文件搜索结束读取 单个字节的解决方案 对于(并计算)新行字符,找到 翻译: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳,校对: 奇芳
  4. 多字节缓冲区多字节缓冲区解决方案针对大文件进行了优化,已找到 这里
  5. 缓冲区长度为 动态的,根据要检索的行数来决定。

所有解决方案 工作。在这个意义上,它们从 对于任何文件和我们要求的任意数量的行(除了解决方案 # 1之外,它可以 在大文件的情况下打破 PHP 内存限制,不返回任何内容) 更好?

性能测试

为了回答这个问题,我做了些测试,这些东西就是这么做的,不是吗?

我准备了一个样本 100KB 文件连接在一起发现不同的文件 然后我编写了一个 PHP 脚本,它使用 检索 1,2,10,20,... 100,200,... ,1000线的五种解决方案 每个单独的测试重复10次(即 类似于 5 × 28 × 10 = 1400测试的东西) ,测量 < strong > average 的运行时间 以微秒为单位的时间

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

Execution time on sample 100 KB log file

解决方案 # 1和 # 2似乎是最糟糕的。解决方案 # 3只有在我们需要的时候才是好的 读几行 解决方案 # 4和 # 5似乎是最好的。 请注意,动态缓冲区大小可以优化算法: 执行时间有点短 因为减少了缓冲区,所以对于很少的行来说更小。

让我们尝试使用一个更大的文件。如果我们必须读取 10MB日志文件会怎么样?

Execution time on sample 10 MB log file

现在,解决方案 # 1是迄今为止最糟糕的一个: 实际上,加载整个10MB 文件 进入内存并不是一个好主意。我运行的测试也在1MB 和100MB 的文件, 情况几乎是一样的。

对于小的日志文件呢? 这是 10KB文件的曲线图:

Execution time on sample 10 KB log file

解决方案 # 1现在是最好的一个! 在内存中加载10KB 不是什么大问题 同样 # 4和 # 5的表现也不错。但这是一个边缘情况: 一个10KB 的日志 意思是150/200行。

您可以下载我所有的测试文件、源代码和结果 这里

最后的想法

强烈建议将解决方案 # 5 用于一般用例: 工作得很好 与每个文件大小,并执行特别好,当读取几行。

避免 < a href = “ https://gist.github.com/d9a609b513d881bdad47”rel = “ noReferrer”> 方案 # 1 ,如果你 应该读取大于10KB 的文件。

解决方案 < strong > < a href = “ https://gist.github.com/5cbf78a4447e1a98b59f”rel = “ noReferrer”> # 2 和 译自: 美国《科学》杂志网站(http://php.net/Manual/en/Function.fSeek.php # 69008) 不是我运行的每个测试的最佳选项: # 2从不运行小于 2毫米,而3英镑受到的影响很大 你问的那些台词(只有1或2行的效果相当不错)。

这也会奏效:

$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();
}

不幸的是,您的 testcase 在我的机器上出现了错误,所以我不能告诉您它的性能如何。

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

/**
* 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);
}

还有一个函数,您可以使用正则表达式来分隔项

$last_rows_array = file_get_tail('logfile.log', 100, array(
'regex'     => true,          // use regex
'separator' => '#\n{2,}#',   //  separator: at least two newlines
'typical_item_size' => 200, //   line length
));

功能:

// public domain
function file_get_tail( $file, $requested_num = 100, $args = array() ){
// default arg values
$regex         = true;
$separator     = null;
$typical_item_size = 100; // estimated size
$more_size_mul = 1.01; // +1%
$max_more_size = 4000;
extract( $args );
if( $separator === null )  $separator = $regex ? '#\n+#' : "\n";


if( is_string( $file ))  $f = fopen( $file, 'rb');
else if( is_resource( $file ) && in_array( get_resource_type( $file ), array('file', 'stream'), true ))
$f = $file;
else throw new \Exception( __METHOD__.': file must be either filename or a file or stream resource');


// get file size
fseek( $f, 0, SEEK_END );
$fsize = ftell( $f );
$fpos = $fsize;
$bytes_read = 0;


$all_items = array(); // array of array
$all_item_num = 0;
$remaining_num = $requested_num;
$last_junk = '';


while( true ){
// calc size and position of next chunk to read
$size = $remaining_num * $typical_item_size - strlen( $last_junk );
// reading a bit more can't hurt
$size += (int)min( $size * $more_size_mul, $max_more_size );
if( $size < 1 )  $size = 1;


// set and fix read position
$fpos = $fpos - $size;
if( $fpos < 0 ){
$size -= -$fpos;
$fpos = 0;
}


// read chunk + add junk from prev iteration
fseek( $f, $fpos, SEEK_SET );
$chunk = fread( $f, $size );
if( strlen( $chunk ) !== $size )  throw new \Exception( __METHOD__.": read error?");
$bytes_read += strlen( $chunk );
$chunk .= $last_junk;


// chunk -> items, with at least one element
$items = $regex ? preg_split( $separator, $chunk ) : explode( $separator, $chunk );


// first item is probably cut in half, use it in next iteration ("junk") instead
// also skip very first '' item
if( $fpos > 0 || $items[0] === ''){
$last_junk = $items[0];
unset( $items[0] );
} // … else noop, because this is the last iteration


// ignore last empty item. end( empty [] ) === false
if( end( $items ) === '')  array_pop( $items );


// if we got items, push them
$num = count( $items );
if( $num > 0 ){
$remaining_num -= $num;
// if we read too much, use only needed items
if( $remaining_num < 0 )  $items = array_slice( $items, - $remaining_num );
// don't fix $remaining_num, we will exit anyway


$all_items[] = array_reverse( $items );
$all_item_num += $num;
}


// are we ready?
if( $fpos === 0 || $remaining_num <= 0 )  break;


// calculate a better estimate
if( $all_item_num > 0 )  $typical_item_size = (int)max( 1, round( $bytes_read / $all_item_num ));
}


fclose( $f );


//tr( $all_items );
return call_user_func_array('array_merge', $all_items );
}

我喜欢下面的方法,但它不会工作的文件高达2GB。

<?php
function lastLines($file, $lines) {
$size = filesize($file);
$fd=fopen($file, 'r+');
$pos = $size;
$n=0;
while ( $n < $lines+1 && $pos > 0) {
fseek($fd, $pos);
$a = fread($fd, 1);
if ($a === "\n") {
++$n;
};
$pos--;
}
$ret = array();
for ($i=0; $i<$lines; $i++) {
array_push($ret, fgets($fd));
}
return $ret;
}
print_r(lastLines('hola.php', 4));
?>

我的小复印粘贴解决方案后,在这里阅读所有这一切。

/**
* @param $pathname
* @param $lines
* @param bool $echo
* @return int
*/
private function tailonce($pathname, $lines, $echo = true)
{
$realpath = realpath($pathname);
$fp = fopen($realpath, 'r', FALSE);
$flines = 0;
$a = -1;
while ($flines <= $lines) {
fseek($fp, $a--, SEEK_END);
$char = fread($fp, 1);
if ($char == "\n") $flines++;
}
$out = fread($fp, 1000000);
fclose($fp);
if ($echo) echo $out;
return $a+2;
}

tail -f中的连续尾部函数
它不关闭 $fp 因为你必须杀死它 反正是 Ctrl-C。 ussleep 节省了你的 CPU 时间,目前为止只在 windows 上测试过。

/**
* @param $pathname
*/
private function tail($pathname)
{
$realpath = realpath($pathname);
$fp = fopen($realpath, 'r', FALSE);
$lastline = '';
fseek($fp, $this->tailonce($pathname, 1, false), SEEK_END);
do {
$line = fread($fp, 1000);
if ($line == $lastline) {
usleep(50);
} else {
$lastline = $line;
echo $lastline;
}
} while ($fp);
}

您需要将这些代码放到一个类中!