-1

I have this php script that reads the csv file, I would like it to start reading from the last line, basically the other way around.

<?php
$f = fopen("./data/data-esp8266-$idluf-$currentdata.csv", "r");
        fgets($f);
while (($line = fgetcsv($f)) !== false) {
       
        $row = $line[0];  // Dobbiamo ottenere la riga effettiva (è il primo elemento in un array di 1 elemento)
        $cells = explode(";",$row);
        echo "<tr>\n";
        foreach ($cells as $cell) {
             echo "<td><a style='text-decoration:none;color:#fff;' class='tooltip' data-tool=' $cell'>" . htmlspecialchars($cell) . "</a></td>\n";
        }
        echo "</tr>\n";
}
fclose($f);
?>
djroby19
  • 3
  • 2
  • I've removed the duplicate close as using the "reverse fseek()" method will not work reliably for CSV unless you are also parsing the CSV in reverse as well. At best it will work in the naive case that the CSV data itself will _never_ contain unescaped line breaks, which is not guaranteed by the format. – Sammitch Aug 02 '23 at 21:03

4 Answers4

0

If the file you're reading were a "simple" file with nothing but line breaks as delimiters, then reading it backwards would be fairly simple. However, CSV is a more complex format with field and row delimiters, enclosures [quotes], and escapes. You could encounter data such as:

id,name,value
1,foo,"hello world"
2,bar,"hello 
world"
3, baz,"""hello 
world"""

Which is perfectly valid CSV, but would break most solutions currently proposed in this thread to read the data backwards, as well as this thread which this question was briefly marked as duplicate of.

The most reliable way to do this would be to read the file forwards first, and then use that information to read it in reverse. The simple version is to just stuff everything into an array and then read that backwards, eg:

$f = fopen("./data/data-esp8266-$idluf-$currentdata.csv", "r");
fgets($f);

$lines = [];
while (($lines[] = fgetcsv($f)) !== false) {}

for( $i=count($lines)-1; $i>=0; --$i ) {
    $line = lines[$i];
    $row = $line[0];  // Dobbiamo ottenere la riga effettiva (è il primo elemento in un array di 1 elemento)
    $cells = explode(";",$row);
    echo "<tr>\n";
    foreach ($cells as $cell) {
        echo "<td><a style='text-decoration:none;color:#fff;' class='tooltip' data-tool=' $cell'>" . htmlspecialchars($cell) . "</a></td>\n";
    }
    echo "</tr>\n";
}
fclose($f);

but if you're processing a large file you may run into memory constraints trying to store all that data.

An alternative would be to read the file forward once first, but only store the offsets in the file for the beginning of the records, and then use those offsets to iterate again in reverse.

function csv_reverse($handle, ...$csv_options) {
    $offsets = [];
    do {
        $offsets[] = ftell($handle);
    } while($row = fgetcsv($handle, ...$csv_options));
    array_pop($offsets); // last offset is EOF
    
    for( $i=count($offsets)-1; $i>=0; --$i ) {
        fseek($handle, $offsets[$i]);
        yield fgetcsv($handle, ...$csv_options);
    }
}

$f = fopen("./data/data-esp8266-$idluf-$currentdata.csv", "r");
fgets($f); // assuming that this discards the header row

$lines = [];
while (($lines[] = fgetcsv($f)) !== false) {}

foreach( csv_reverse($f) as $line ) {
    // same as above
}
fclose($f);

Live example: https://3v4l.org/0sc8q

There is a tradeoff here, in that the file must be traversed twice, but that's going to have to be a necessary evil if there is a memory constraint.

All this said, the better option would be to have this data in a database, if possible, which can trivially re-order the data for you on the fly. This code is already kinda-sorta reimplementing DB-related functionality, but worse.

Edit, because @Your-Common-Sense won't stop bothering me about this.

Here is a first-pass buffered reverse CSV iterator that respects quotes, and escaped quotes [probably] but probably has other less-common edge-case issues.

define('DEBUG', false);

function chunk_read_reverse($handle, $bufsz=4096) {
    fseek($handle, 0, SEEK_END);
    do{
        $cur = ftell($handle);
        if( $cur - $bufsz <= 0 ) {
            if( $cur == 0 ) { return; }
            fseek($handle, 0);
            yield fread($handle, $cur);
            return;
        } else {
            fseek($handle, -1 * $bufsz, SEEK_CUR);
        }
        $buffer = fread($handle, $bufsz);
        yield $buffer;
        fseek($handle, -1 * $bufsz, SEEK_CUR);
    } while(true);
}

function csv_reverse_iterate($handle, ...$csv_options) {
    $quote   = $csv_options[1] ?? '"';
    $endl    = "\n";
    $buffer  = '';
    $end     = 0;
    $q_count = 0;
    foreach(chunk_read_reverse($handle) as $chunk) {
        $buffer = $chunk . $buffer;
        $buflen = strlen($buffer);
        if(DEBUG){printf("bufflen: %4d, end: %4d\n", $buflen, $end);}
        for( $i=strlen($chunk)-1; $i>=0; --$i ) {
            $c = $buffer[$i];
            if(DEBUG){printf("i: %3d, e: %3d, c: %1s, x: %s, q: %d\n", $i, $end, trim($c), bin2hex($c), $q_count);}
            if( $c === $quote ) {
                ++$q_count;
            }
            else if( $c === $endl && $q_count % 2 == 0 ) {
                yield str_getcsv(substr($buffer, $i+1, ($buflen-$end)-($i+1)), ...$csv_options);
                $end = $buflen - $i;
            }
        }
        $buffer = substr($buffer, 0, $buflen-$end);
        $end = 0;
    }
    yield str_getcsv($buffer, ...$csv_options);
}

$csv = <<<_E_
id,name,value
1,foo,"hello world"
2,bar,"hello 
world"
3, baz,"""hello 
world"""
_E_;

$in = fopen('php://memory', 'rwb');
fwrite($in, $csv);
rewind($in);

foreach( csv_reverse_iterate($in) as $row ) {
    var_dump($row);
}

I don't necessarily recommend using this, as it only cares about quotes and line breaks and does not account for other potential format quirks. But if you want to be pedantic like @Your-Common-Sense and save a questionably-significant amount of processing time, then you can use this slightly-worse-but-maybe-slightly-faster code.

Let me reiterate: If you need to do this enough that a few extra milliseconds makes a difference, then you should almost certainly be using a database, or database-like solution, and not CSV/flat files. This code was written purely out of spite.

Sammitch
  • 30,782
  • 7
  • 50
  • 77
  • fgetcsv is awfully slow alone, and you are running it twice – Your Common Sense Aug 03 '23 at 10:27
  • A native solution would be 2 times faster, if implements a buffer-based backward reading principle featured in a *former duplicate* – Your Common Sense Aug 03 '23 at 10:33
  • Yes, and it would _still_ break on a line break inside of a field. If you know of a way to parse CSV faster and/or backwards go ahead and add a better answer. – Sammitch Aug 03 '23 at 18:05
  • Not sure what makes you think it would break. Probably you didn't bother to check how it actually works – Your Common Sense Aug 03 '23 at 18:14
  • Oh yes, it works _flawlessly_. https://3v4l.org/k5ftF – Sammitch Aug 03 '23 at 18:31
  • I assume you took this code from the accepted answer. But from experience, we can tell that top voted answer is not necessarily the best/useful one. – Your Common Sense Aug 03 '23 at 18:38
  • They are all various flavors of "iterate in reverse by line breaks" which will _all still break_ in this case because they're not format-aware. At this point either post working code, or drop it. – Sammitch Aug 03 '23 at 18:41
  • it's not a "reverse by line breaks" which is quite dumb, because reads by one byte at a time. But, like I said, a smart solution utilizing a buffer-based backward reading principle when data for parsing is read by chunks – Your Common Sense Aug 03 '23 at 18:55
  • Yes, that is a slightly different way to phrase the wrong answer. Where code? – Sammitch Aug 03 '23 at 19:15
  • @YourCommonSense Don't worry about the code. I posted it for you already. – Sammitch Aug 03 '23 at 20:32
-2

I think that you just need to read the file backwards line by line As shown here Read a file backwards line by line using fseek

If you're willing to load the whole file into memory you can use file_get_contents(<path>); , split the file by line end character and read the resulting array from len back down to 1

in need of help
  • 1,606
  • 14
  • 27
  • If you're willing to read the whole thing into memory you might as well read it normally, store the data in an array, and iterate the array in reverse. Also, making suggestions and/or linking to other resources does not qualify as an answer. – Sammitch Aug 02 '23 at 19:06
-2

Or you can use loop with for:

<?php
$lines = file("./data/data-esp8266-$idluf-$currentdata.csv", FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

// Loop through the lines array in reverse
for ($i = count($lines)-1; $i >= 0; $i--) {
    $row = $lines[$i];
    //$cells = explode(";", $row); // We can use the function explode() only if ; is not inside a quoted string
    //$cells = preg_split('/;(?=(?:[^"]*"[^"]*")*[^"]*$)/', $row); // Otherwise, we can use a regular expression (for big files it's more faster than str_getcsv)
    $cells = str_getcsv( $row,  ";", "\"", "\\" ); // Also, we can use the function str_getcsv
    echo "<tr>\n";
    foreach ($cells as $cell) {
        // $cell = trim($cell, "\""); // Only if we use regular expressions
        echo "<td><a style='text-decoration:none;color:#fff;' class='tooltip' data-tool=' $cell'>" . htmlspecialchars($cell) . "</a></td>\n";

    }
    echo "</tr>\n";
}
?>

This method should be faster than the other way based on the function array_reverse()

Oleh Kosarenko
  • 436
  • 2
  • 7
-3

If file is relatively small, you can read whole file into memory using file, than reverse it and loop:

$csvLines = file("./data/data-esp8266-{$idluf}-{$currentdata}.csv",  FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

$csvLines = array_reverse($csvLines);

foreach ($csvLines as $csvLine) {
    $line = str_getcsv($csvLine);

    /* ... */
}
Justinas
  • 41,402
  • 5
  • 66
  • 96