在我的 PHP 应用程序中,我需要从许多文件(主要是日志)的末尾开始读取多行。有时我只需要最后一个,有时我需要几十个或几百个。基本上,我想要像 Unixtail
命令一样灵活的东西。
这里有关于如何从文件中获取最后一行的问题(但我需要N行),并给出了不同的解决方案。我不确定哪个是最好的,哪个表现更好。
在我的 PHP 应用程序中,我需要从许多文件(主要是日志)的末尾开始读取多行。有时我只需要最后一个,有时我需要几十个或几百个。基本上,我想要像 Unixtail
命令一样灵活的东西。
这里有关于如何从文件中获取最后一行的问题(但我需要N行),并给出了不同的解决方案。我不确定哪个是最好的,哪个表现更好。
在互联网上搜索,我遇到了不同的解决方案。我可以将它们分为三种方法:
file()
PHP 函数的天真者;tail
命令的作弊者;fseek()
.我最终选择(或编写)五个解决方案,一个幼稚的解决方案,一个作弊的解决方案和三个强大的解决方案。
tail
command的唯一可能的解决方案,它有一个小问题:如果tail
不可用,它就不会运行,即在非 Unix (Windows) 或不允许系统功能的受限环境中运行。所有解决方案都有效。从某种意义上说,它们从任何文件以及我们要求的任意数量的行返回预期结果(除了解决方案#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)上运行脚本。结果如下:
解决方案#1 和#2 似乎更糟糕。解决方案#3 仅在我们需要阅读几行代码时才有用。解决方案#4 和#5 似乎是最好的。 请注意动态缓冲区大小如何优化算法:由于缓冲区减少,几行执行时间会稍微小一些。
让我们尝试一个更大的文件。如果我们必须读取10 MB的日志文件怎么办?
现在解决方案 #1 是最糟糕的一个:事实上,将整个 10 MB 文件加载到内存中并不是一个好主意。我也在 1MB 和 100MB 文件上运行测试,情况几乎相同。
对于微小的日志文件?这是10 KB文件的图表:
解决方案 #1 现在是最好的解决方案!将 10 KB 加载到内存中对 PHP 来说并不是什么大问题。#4和#5也表现良好。然而,这是一个极端情况:10 KB 的日志意味着 150/200 行......
你可以在这里下载我所有的测试文件、来源和结果 。
强烈建议将解决方案 #5用于一般用例:适用于每种文件大小,并且在读取几行时表现特别好。
如果您应该读取大于 10 KB 的文件,请避免使用解决方案 #1 。
解决方案#2 和#3 并不是我运行的每个测试的最佳解决方案:#2 的运行时间不会少于 2 毫秒,而 #3 很大程度上受您询问的行数的影响(仅在 1 或 2 行时效果很好)。
这是一个修改后的版本,它也可以跳过最后几行:
/**
* 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);
}
这也可以:
$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();
}
不幸的是,你的测试用例在我的机器上出现了段错误,所以我不知道它是如何执行的。
我喜欢下面的方法,但它不适用于最大 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));
?>
在这里阅读所有内容后,我的小复制粘贴解决方案。tail() 不会关闭 $fp ,因为无论如何您都必须使用 Ctrl-C 将其杀死。为节省您的 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);
}
/**
* @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;
}
还有另一个功能,您可以使用正则表达式来分隔项目。用法
$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 );
}
对于常规的小文本文件,一个衬垫,不用担心:
echo join(array_slice(file("path/to/file"), -5));
要定义新行,根据上下文,这种方式通常更容易:
echo join("\n",array_slice(explode("\n",file_get_contents("path/to/file")), -5));
echo join("<br>",array_slice(explode(PHP_EOL,file_get_contents("path/to/file")), -5));
echo join(PHP_EOL,array_slice(explode("\n",file_get_contents("path/to/file")), -5));