Was ist der beste Weg in PHP, um die letzten Zeilen aus einer Datei zu lesen?

75

In meiner PHP-Anwendung muss ich mehrere Zeilen ab dem Ende vieler Dateien (meistens Protokolle) lesen . Manchmal brauche ich nur den letzten, manchmal brauche ich zehn oder Hunderte. Grundsätzlich möchte ich etwas so Flexibles wie den Unix- tail Befehl.

Hier gibt es Fragen zum Abrufen der letzten Zeile aus einer Datei (ich benötige jedoch N Zeilen), und es wurden verschiedene Lösungen angegeben. Ich bin mir nicht sicher, welches das beste ist und welches besser abschneidet.

Lorenzo-s
quelle
$file = file('filename.txt'); echo $file[count($file) - 1];
Winston
@ Winston Grundsätzlich ist das die Lösung Nr. 1 in meiner Antwort. Probieren Sie es aus, für große Dateien ist es absolut zu vermeiden!
Lorenzo-s
1
PHP Tail Bibliothek macht dies ganz einfach: packagist.org/packages/icyapril/tail
mjsa

Antworten:

265

Methodenübersicht

Bei der Suche im Internet bin ich auf verschiedene Lösungen gestoßen. Ich kann sie in drei Ansätze einteilen:

  • naive , die file()PHP-Funktion verwenden;
  • Betrüger , die tailBefehle auf dem System ausführen;
  • Mächtige , die glücklich mit einer geöffneten Datei herumspringen fseek().

Am Ende habe ich fünf Lösungen ausgewählt (oder geschrieben), eine naive , eine betrügerische und drei mächtige .

  1. Die prägnanteste naive Lösung mit integrierten Array-Funktionen.
  2. Die einzig mögliche Lösung basierend auftail Befehlen , die ein kleines großes Problem hat: Sie wird nicht ausgeführt, wenn sie tailnicht verfügbar ist, dh unter Nicht-Unix (Windows) oder in eingeschränkten Umgebungen, in denen keine Systemfunktionen zulässig sind.
  3. Die Lösung, in der einzelne Bytes vom Ende der Datei gelesen werden, um nach Zeilenumbrüchen zu suchen (und diese zu zählen), finden Sie hier .
  4. Die Multi-Byte - gepufferten Lösung für große Dateien optimiert, fand hier .
  5. Eine leicht modifizierte Version von Lösung Nr. 4, bei der die Pufferlänge dynamisch ist, wird entsprechend der Anzahl der abzurufenden Zeilen festgelegt.

Alle Lösungen funktionieren . In dem Sinne, dass sie das erwartete Ergebnis aus jeder Datei und für eine beliebige Anzahl von Zeilen zurückgeben, die wir anfordern (mit Ausnahme der Lösung Nr. 1, die bei großen Dateien die PHP-Speichergrenzen überschreiten kann und nichts zurückgibt). Aber welches ist besser?

Leistungstests

Um die Frage zu beantworten, führe ich Tests durch. So wird das gemacht, nicht wahr?

Ich habe eine Probe vorbereitet 100 KB in der verschiedene Dateien aus meinem /var/logVerzeichnis zusammengefügt wurden. Dann schrieb ich ein PHP-Skript, das jede der fünf Lösungen verwendet, um 1, 2, .., 10, 20, ... 100, 200, ..., 1000 Zeilen vom Ende der Datei abzurufen . Jeder einzelne Test wird zehnmal wiederholt (das entspricht etwa 5 × 28 × 10 = 1400 Tests), wobei die durchschnittliche verstrichene Zeit in Mikrosekunden gemessen wird .

Ich führe das Skript auf meinem lokalen Entwicklungscomputer (Xubuntu 12.04, PHP 5.3.10, Dual-Core-CPU mit 2,70 GHz, 2 GB RAM) mit dem PHP-Befehlszeileninterpreter aus. Hier sind die Ergebnisse:

Ausführungszeit für eine 100-KB-Beispielprotokolldatei

Lösung Nr. 1 und Nr. 2 scheinen die schlechteren zu sein. Lösung 3 ist nur dann gut, wenn wir einige Zeilen lesen müssen. Die Lösungen Nr. 4 und Nr. 5 scheinen die besten zu sein. Beachten Sie, wie die dynamische Puffergröße den Algorithmus optimieren kann: Die Ausführungszeit ist aufgrund des reduzierten Puffers für einige Zeilen etwas kleiner.

Versuchen wir es mit einer größeren Datei. Was ist, wenn wir a lesen müssen? 10-MB- Protokolldatei ?

Ausführungszeit für Beispiel-10-MB-Protokolldatei

Jetzt ist Lösung Nr. 1 bei weitem die schlechtere: Tatsächlich ist das Laden der gesamten 10-MB-Datei in den Speicher keine gute Idee. Ich führe die Tests auch für 1 MB- und 100 MB-Dateien aus, und es ist praktisch die gleiche Situation.

Und für winzige Protokolldateien? Das ist die Grafik für a 10-KB- Datei:

Ausführungszeit für Beispiel-10-KB-Protokolldatei

Lösung Nr. 1 ist jetzt die beste! Das Laden von 10 KB in den Speicher ist für PHP keine große Sache. Auch # 4 und # 5 schneiden gut ab. Dies ist jedoch ein Randfall: Ein 10-KB-Protokoll bedeutet ungefähr 150/200 Zeilen ...

Sie können alle meine Testdateien, Quellen und Ergebnisse hier herunterladen .

Abschließende Gedanken

Lösung Nr. 5 wird für den allgemeinen Anwendungsfall dringend empfohlen: Funktioniert hervorragend bei jeder Dateigröße und ist besonders gut beim Lesen einiger Zeilen.

Vermeiden Lösung 1, wenn Sie Dateien lesen sollten, die größer als 10 KB sind.

Lösung Nr. 2 und Nr. 3 sind nicht die besten für jeden Test, den ich durchführe: Nr. 2 läuft nie in weniger als 2 ms, und Nr. 3 wird stark von der Anzahl der von Ihnen angeforderten Zeilen beeinflusst (funktioniert nur mit 1 oder 2 Zeilen recht gut ).

Lorenzo-s
quelle
Übrigens, wie wäre es, wenn Sie den Code auf BitBucket oder so etwas anstatt in eine nervige Zip-Datei setzen? : p
Svish
Auch ... nicht ganz sicher, ob Ihre Optimierung wirklich so notwendig ist, hehe. Nicht so viel Unterschied.
Svish
5
@Svish Der Code befindet sich auf einem GitHub Gist. Wenn Sie über die gesamten Testdateien sprechen, denke ich, dass es nicht notwendig ist, sie in ein Repo zu setzen ... Über die Optimierung: Ich wollte mich wirklich auf die Leistung konzentrieren, weil ich diesen Code sehr intensiv für wenige Zeilen lesen musste (weniger als 10). Ein großer Puffer schien mir also unnötig. Beachten Sie, dass die Achse logarithmisch ist: Für wenige Zeilen bedeutet ein reduzierter Puffer die Hälfte der Ausführungszeit!
Lorenzo-s
Es fasst alle Zeilen zu einer zusammen. Können wir Zeilenumbrüche beibehalten?
FractalSpace
10
möglicherweise eine der besten SO-Antworten, die ich je gesehen habe. Optionen, mehrere Tests, Schlussfolgerungen. Du brauchst eine Medaille.
David
5

Dies ist eine modifizierte Version, die auch die letzten Zeilen überspringen kann:

/**
 * 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);
}
Kinga die Hexe
quelle
4

Dies würde auch funktionieren:

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

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

Leider ist Ihr Testfall auf meinem Computer fehlerhaft, sodass ich nicht sagen kann, wie er funktioniert.

Gordon
quelle
1
Ich wusste nichts über die SplFileObjectKlasse, danke. Ich weiß nicht, warum der Test auf Ihrem Computer fehlerhaft ist, trotzdem führe ich ihn zusammen mit der besseren Methode (Nr. 5) für die 10-MB-Datei aus, und die Leistung ist nicht ganz gut, sie ist vergleichbar mit der Shell-Methode (Nr. 2). Siehe hier .
Lorenzo-s
Beachten Sie, dass die erste Lösung mit LimitIteratorausgelöst wird, OutOfRangeExceptionwenn Ihre Datei weniger als 20 Zeilen enthält Parameter offset must be >= 0. Der zweite wird LogicExceptionaus dem gleichen Grund werfen .
Georgy Ivanov
1

Meine kleine Lösung zum Kopieren und Einfügen, nachdem ich das alles hier gelesen habe. tail () schließt $ fp nicht, da Sie es trotzdem mit Strg-C beenden müssen. usleep zur Einsparung Ihrer CPU-Zeit, bisher nur unter Windows getestet. Sie müssen diesen Code in eine Klasse einfügen!

/**
 * @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;
}
user163193
quelle
0

Eine weitere Funktion: Sie können reguläre Ausdrücke verwenden, um Elemente zu trennen. Verwendung

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

Die Funktion:

// 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 );
}
biziclop
quelle
0

Ich mag die folgende Methode, aber sie funktioniert nicht bei Dateien mit bis zu 2 GB.

<?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));
?>
Sergiotarxz
quelle
0

Bei normalen kleinen Textdateien ist der eine Liner kein Grund zur Sorge:

echo join(array_slice(file("path/to/file"), -5));

Um die neuen Zeilen zu definieren, ist es je nach Kontext oft einfacher:

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));
NVRM
quelle