Read a file backwards line by line using fseek
Asked Answered
K

10

15

How do I read a file backwards line by line using fseek?

code can be helpful. must be cross platform and pure php.

many thanks in advance

regards

Jera

Knowlton answered 13/7, 2010 at 6:6 Comment(2)
Is there any reason why it must be seeked rather than the suggestions below involving reading the whole file? What kind of data are you reading?Wilberwilberforce
If you want more memory-friendly solution - here's a answer of mine that answers similar question : #20561980Walls
I
23

The question is asking using fseek, so can only assume that performance is an issue and file() is not the solution. Here is a simple approach using fseek:

My file.txt

#file.txt
Line 1
Line 2
Line 3
Line 4
Line 5

And the code:

<?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);

Output:

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"
}

You don't have to append to the $lines array like I am, you can print the output straight away if that is the purpose of your script. Also it is easy to introduce a counter if you want to limit the number of lines.

$linesToShow = 3;
$counter = 0;
while ($counter <= $linesToShow && -1 !== fseek($fp, $pos, SEEK_END)) {
   // Rest of code from example. After $lines[] = $currentLine; add:
   $counter++;
}
Indenture answered 22/2, 2013 at 5:48 Comment(2)
After my edit was rejected...: You should check if the file handler is valid after fopen() and later free with fclose() to release it to prevent memory leakage.Gillett
PHP_EOL is platform dependant and in some cases will be insufficent. Replacing condition with ... ("\n" == $char) ... and skipping empty lines will make it more reliableMarylou
N
23

If you are going to read the entire file in anyways, just use file() to read the file in as an array (each line is each element in the array) and then use array_reverse() to flip the array backwards and loop through that. Or just do a reverse for loop where you start at the end and decrement on each loop.

$file = file("test.txt");
$file = array_reverse($file);
foreach($file as $f){
    echo $f."<br />";
}
Numbles answered 13/7, 2010 at 6:20 Comment(7)
Or to avoid reversing the array, you could just loop from $i = count($file) - 1; $i > 0; $i++) and read $file[$i]Metaphase
@EqSum yep, can do that. Middle part of for loop should be $i >= 0 though.Numbles
Hey! If you're parsing a really big file (millions of lines) then this will quickly use up loads of memory by loading the whole thing into memory.This is not a good idea!Pillar
@AustinBurk yes, but like I said, "if you are going to read the entire file in anyways...". As in if you were going to read the entire file into memory anyways, this wouldn't use any more memory than reading the file one line at a time and loading that into memory.Numbles
I know it's old question, but I thought I'd share a comment under this answer. It's the opposite of array_slice and on both array_slice and array_reverse for files of small, average and above average in size it does the trick. Best answer for 99% of cases, only huge files will exceed either memory or time limit. ( ex:32MB is huge for txt file )Shavian
array_slice is really only related to array_reverse in that they both work on arrays. They don't really do the same thing at all. The array_reverse here was just for readability. If you didn't want it, since file produces a numeric array you could just do a for loop decrementing your iterator like for($i=count($file)-1; $i>=0; $i--){ $line = $file[$i]; /*do something*/ }.Numbles
good answer (for some people), but for wrong question (not using fseek)Gilford
I
23

The question is asking using fseek, so can only assume that performance is an issue and file() is not the solution. Here is a simple approach using fseek:

My file.txt

#file.txt
Line 1
Line 2
Line 3
Line 4
Line 5

And the code:

<?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);

Output:

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"
}

You don't have to append to the $lines array like I am, you can print the output straight away if that is the purpose of your script. Also it is easy to introduce a counter if you want to limit the number of lines.

$linesToShow = 3;
$counter = 0;
while ($counter <= $linesToShow && -1 !== fseek($fp, $pos, SEEK_END)) {
   // Rest of code from example. After $lines[] = $currentLine; add:
   $counter++;
}
Indenture answered 22/2, 2013 at 5:48 Comment(2)
After my edit was rejected...: You should check if the file handler is valid after fopen() and later free with fclose() to release it to prevent memory leakage.Gillett
PHP_EOL is platform dependant and in some cases will be insufficent. Replacing condition with ... ("\n" == $char) ... and skipping empty lines will make it more reliableMarylou
D
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";
Dwelt answered 8/5, 2012 at 7:58 Comment(0)
M
8

To completely reverse a file:

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

Of course, you wanted line-by-line reversal...


$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") {
        // analyse completed line $output[$ln] if need be
        $ln++;
        continue;
        }
    $output[$ln] = $char . ((array_key_exists($ln, $output)) ? $output[$ln] : '');
    }
fclose($fl);
print_r($output);

Really though, Jonathan Kuhn has the best answer IMHO above. The only cases you'd not use his answer that I know of is if file or like functions are disabled via php.ini, yet the admin forgot about fseek, or when opening a huge file just get the last few lines of contents would magically save memory this way.

Note: Error handling not included. And, PHP_EOL didn't cooperate, so I used "\n" to denote end of line instead. So, above may not work in all cases.

Myrta answered 13/7, 2010 at 7:5 Comment(1)
The code from the 2nd part, which analyses line by line isn't working.Phenomena
R
6

You cannot fseek line by line, because you do not know how long the lines are until you read them.

You should either read the whole file into a list of lines, or if the file is too big for that and you only need the last lines, read fixed-sized chunks from the end of the file and implement a bit more complicated logic which detects lines from such data.

Recti answered 13/7, 2010 at 6:13 Comment(0)
B
2

Reading the entire file into an array and reversing is fine unless the file is enormous.

You could perform a buffered read of your file from back to front with something like this:

  • establish a buffer_size B - this should be longer than the longest anticipated line otherwise you'll need some logic for growing the buffer size when lines are too long
  • set offset = file length - buffer_size
  • while the offset>=0
    • read buffer_size bytes from offset
    • read a line - it will be incomplete as we'll have jumped into the middle of a line, so we want to ensure the next buffer we read ends with it. Set offset = offset - buffer_size + line length
    • discard that line, read all following lines into an array and reverse them
    • process this array to do whatever you wanted to do
Bonita answered 13/7, 2010 at 7:40 Comment(0)
H
0

This code read file backwards. This code ignore modifications on reading, example apache access.log new lines on procressing.

$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);
Headroom answered 10/1, 2015 at 21:47 Comment(0)
G
0

Here's a drop in replacement(ish) for fgets($fp) called fgetsr() that reads lines from a file in reverse order.

This code is verbatim so you should (famous last words) be able to copy it into a file on your server and run it. Though you may well need to change the filename in the fopn() call.

<?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;
            }
        }
    }
?>
Gulosity answered 15/11, 2020 at 10:32 Comment(0)
A
0

Functions to read a file line-by-line in reverse:

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;
}

Example use case: parse a long file until some date:

$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);
Alper answered 9/10, 2021 at 9:17 Comment(0)
F
-1

I know this has been answered already but I found another, maybe faster, way.

// 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);
}
Flan answered 1/11, 2019 at 9:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.