使用fseek逆向按行读取文件

15

如何使用 fseek 逆序逐行读取文件?

请提供可跨平台且纯 PHP 的代码。

非常感谢!

祝好

Jera


有没有什么理由必须使用查找而不是涉及读取整个文件的以下建议?你正在读取什么类型的数据? - salathe
如果您想要更加节省内存的解决方案 - 这是我的一个回答,回答了类似的问题:https://dev59.com/xWIj5IYBdhLWcg3wKiLs#26595154 - jave.web
10个回答

23

如果你要读取整个文件,那么可以使用file()将文件读入一个数组中(每一行是数组的一个元素),然后使用array_reverse()将数组翻转并循环遍历。或者只需使用逆序for循环,从末尾开始循环并在每次循环中递减。

$file = file("test.txt");
$file = array_reverse($file);
foreach($file as $f){
    echo $f."<br />";
}

1
或者为了避免反转数组,你可以从 $i = count($file) - 1; $i > 0; $i++) 循环,并读取 $file[$i]。 - rodrigo-silveira
@EqSum 好的,可以做到。for循环的中间部分应该是$i >= 0 - Jonathan Kuhn
2
嘿!如果您正在解析一个非常大的文件(数百万行),那么通过将整个文件加载到内存中,这很快会使用大量内存。这不是一个好主意! - Austin Burk
1
@AustinBurk 是的,但就像我说的那样,“如果你无论如何都要读取整个文件...”。也就是说,如果你无论如何都要将整个文件读入内存,那么这种方法不会比一次读取一行并将其加载到内存中使用更多的内存。 - Jonathan Kuhn
array_slicearray_reverse 只是在它们都处理数组方面有关联。它们实际上并不做同样的事情。这里的 array_reverse 只是为了可读性。如果你不想要它,因为 file 生成一个数字数组,你可以使用一个 for 循环来递减迭代器,像这样 for($i=count($file)-1; $i>=0; $i--){ $line = $file[$i]; /*do something*/ } - Jonathan Kuhn
显示剩余2条评论

23
问题在于要使用fseek,因此可以推断性能是一个问题,而file()不是解决方案。这里有一个使用fseek的简单方法:
我的file.txt
#file.txt
Line 1
Line 2
Line 3
Line 4
Line 5

而且代码:

<?php

$fp = fopen('file.txt', 'r');

$pos = -2; // Skip final new line character (Set to -1 if not present)

$lines = array();
$currentLine = '';

while (-1 !== fseek($fp, $pos, SEEK_END)) {
    $char = fgetc($fp);
    if (PHP_EOL == $char) {
            $lines[] = $currentLine;
            $currentLine = '';
    } else {
            $currentLine = $char . $currentLine;
    }
    $pos--;
}

$lines[] = $currentLine; // Grab final line

var_dump($lines);

输出:

array(5) {
   [0]=>
   string(6) "Line 5"
   [1]=>
   string(6) "Line 4"
   [2]=>
   string(6) "Line 3"
   [3]=>
   string(6) "Line 2"
   [4]=>
   string(6) "Line 1"
}

您不必像我一样将内容添加到$lines数组中,如果您的脚本的目的是打印输出,您可以直接打印。此外,如果您想限制行数,很容易引入计数器。

$linesToShow = 3;
$counter = 0;
while ($counter <= $linesToShow && -1 !== fseek($fp, $pos, SEEK_END)) {
   // Rest of code from example. After $lines[] = $currentLine; add:
   $counter++;
}

1
在我的编辑被拒绝后...:你应该在fopen()之后检查文件处理器是否有效,并在稍后使用fclose()释放它以防止内存泄漏。 - StanE
1
PHP_EOL 是平台相关的,在某些情况下可能不足。将条件替换为 ... ("\n" == $char) ... 并跳过空行将使其更可靠。 - ANTARA

18
<?php

class ReverseFile implements Iterator
{
    const BUFFER_SIZE = 4096;
    const SEPARATOR = "\n";

    public function __construct($filename)
    {
        $this->_fh = fopen($filename, 'r');
        $this->_filesize = filesize($filename);
        $this->_pos = -1;
        $this->_buffer = null;
        $this->_key = -1;
        $this->_value = null;
    }

    public function _read($size)
    {
        $this->_pos -= $size;
        fseek($this->_fh, $this->_pos);
        return fread($this->_fh, $size);
    }

    public function _readline()
    {
        $buffer =& $this->_buffer;
        while (true) {
            if ($this->_pos == 0) {
                return array_pop($buffer);
            }
            if (count($buffer) > 1) {
                return array_pop($buffer);
            }
            $buffer = explode(self::SEPARATOR, $this->_read(self::BUFFER_SIZE) . $buffer[0]);
        }
    }

    public function next()
    {
        ++$this->_key;
        $this->_value = $this->_readline();
    }

    public function rewind()
    {
        if ($this->_filesize > 0) {
            $this->_pos = $this->_filesize;
            $this->_value = null;
            $this->_key = -1;
            $this->_buffer = explode(self::SEPARATOR, $this->_read($this->_filesize % self::BUFFER_SIZE ?: self::BUFFER_SIZE));
            $this->next();
        }
    }

    public function key() { return $this->_key; }
    public function current() { return $this->_value; }
    public function valid() { return ! is_null($this->_value); }
}

$f = new ReverseFile(__FILE__);
foreach ($f as $line) echo $line, "\n";

8
完全反转文件的方法如下:
$fl = fopen("\some_file.txt", "r");
for($x_pos = 0, $output = ''; fseek($fl, $x_pos, SEEK_END) !== -1; $x_pos--) {
    $output .= fgetc($fl);
    }
fclose($fl);
print_r($output);

当然,如果你想要逐行反转...
$fl = fopen("\some_file.txt", "r");
for($x_pos = 0, $ln = 0, $output = array(); fseek($fl, $x_pos, SEEK_END) !== -1; $x_pos--) {
    $char = fgetc($fl);
    if ($char === "\n") {
        // 分析完成的一行内容 $output[$ln]
        $ln++;
        continue;
        }
    $output[$ln] = $char . ((array_key_exists($ln, $output)) ? $output[$ln] : '');
    }
fclose($fl);
print_r($output);

实际上,Jonathan Kuhn 提供的答案是我认为最好的。我所知道的唯一不使用他的答案的情况是,如果通过php.ini禁用了file或类似函数,但管理员忘记了fseek,或者当打开一个巨大的文件只获取最后几行内容时,这种方法可以神奇地节省内存。
注意:未包含错误处理。PHP_EOL没有合作,因此我使用"\n"来表示行末。因此,上述代码可能无法在所有情况下正常工作。

第二部分逐行分析的代码无法正常工作。 - Nelson Teixeira

6

您不能逐行使用fseek,因为在读取它们之前,您不知道每行有多长。

您应该将整个文件读入行列表中,或者如果文件太大而且您只需要最后几行,则从文件末尾读取固定大小的块,并实现更复杂的逻辑以从这些数据中检测出行。


2
将整个文件读入数组并进行反转,这种方法在处理大型文件时效率低下。
你可以实现从后往前对文件进行缓冲读取,类似于以下步骤:
- 确定缓冲区大小B(应长于预期最长行),否则当行太长时需要逻辑来增加缓冲区大小。 - 设置偏移量offset = 文件长度 - 缓冲区大小 - 当 offset >= 0 时执行以下操作:
- 从 offset 处读取buffer_size字节 - 读取一行 - 这将是不完整的,因为我们已经跳进了一行的中间,所以我们要确保下一个读取的缓冲区以此行结尾。设置 offset = offset - buffer_size + 行长度 - 抛弃该行,将所有后续行读入数组并反转它们 - 处理该数组以完成你想做的操作

0

这段代码可以倒序读取文件。在读取时,该代码会忽略修改,例如处理 Apache access.log 时的新行。

$f = fopen('FILE', 'r');

fseek($f, 0, SEEK_END);

$pos = ftell($f);
$pos--;

while ($pos > 0) {
    $chr = fgetc($f);
    $pos --;

    fseek($f, $pos);

    if ($chr == PHP_EOL) {
        YOUR_OWN_FUNCTION($rivi);
        $rivi = NULL;
        continue;
    }

    $rivi = $chr.$rivi;
}

fclose($f);

0

逆序逐行读取文件的函数:

function revfopen($filepath, $mode)
{
    $fp = fopen($filepath, $mode);
    fseek($fp, -1, SEEK_END);
    if (fgetc($fp) !== PHP_EOL) {
        fseek($fp, 1, SEEK_END);
    }

    return $fp;
}

function revfgets($fp)
{
    $s = '';
    while (true) {
        if (fseek($fp, -2, SEEK_CUR) === -1) {
            return false;
        }
        if (($c = fgetc($fp)) === PHP_EOL) {
            break;
        }
        $s = $c . $s;
    }

    return $s;
}

示例用例:解析长文件直到某个日期:

$fp = revfopen('/path/to/file', 'r');

$buffer = '';
while (($line = revfgets($fp)) !== false) {
    if (strpos($line, '05-10-2021') === 0) {
        break;
    }

    array_unshift($buffer, $line);
}

echo implode("\n", $buffer);

0

这里有一个名为fgetsr()的替代fgets($fp)的函数,可以倒序读取文件中的行。

这段代码是逐字逐句的,所以你应该(最后的话)能够将其复制到服务器上的文件中并运行它。不过你可能需要在fopn()调用中更改文件名。

<?php
    header('Content-Type: text/plain');
    $fp = fopen('post.html', 'r');
    
    while($line = fgetsr($fp)) {
        echo $line;
    }







    // Read a line from the file but starting from the end
    //
    // @param $fp integer The file pointer
    //
    function fgetsr($fp)
    {
        // Make this variable persistent inside this function
        static $seeked;
        
        // The line buffer that will eventually be returned
        $line = '';

        // Initially seek to the end of the file
        if (!$seeked) {
            fseek($fp, -1, SEEK_END);
            $seeked = true;
        }
        
        // Loop through all of the characters in the file
        while(strlen($char = fgetc($fp)) {

            // fgetc() advances that pointer so go back TWO places
            // instead of one
            fseek($fp, -2, SEEK_CUR);

            //
            // Check for a newline (LF). If a newline is found
            // then break out of the function and return the
            // line that's stored in the buffer.
            //
            // NB The first line in the file (ie the last to
            //    be read)has a special case
            //
            if (ftell($fp) <= 0) {
                fseek($fp, 0, SEEK_SET);
                $line = fgets($fp);
                fseek($fp, 0, SEEK_SET);
                return $line;
            } else if ($char === "\n") {
                $line = strrev($line);
                return $line . "\n";
            } else {
                $line .= $char;
            }
        }
    }
?>

-1

我知道这个问题已经有答案了,但我发现另外一种可能更快的方法。

// Read last 5000 chars of 'foo.log' 

if(file_exists('foo.log') && $file = fopen('foo.log', 'r')) {
    fseek($file, -5000, SEEK_END);

    $text = stream_get_line($file, 5000); 

    var_dump($text);

    fclose($file);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接