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.
php
performance
logging
Lorenzo-s
quelle
quelle
$file = file('filename.txt'); echo $file[count($file) - 1];
Antworten:
Methodenübersicht
Bei der Suche im Internet bin ich auf verschiedene Lösungen gestoßen. Ich kann sie in drei Ansätze einteilen:
file()
PHP-Funktion verwenden;tail
Befehle auf dem System ausführen;fseek()
.Am Ende habe ich fünf Lösungen ausgewählt (oder geschrieben), eine naive , eine betrügerische und drei mächtige .
tail
Befehlen , die ein kleines großes Problem hat: Sie wird nicht ausgeführt, wenn sietail
nicht verfügbar ist, dh unter Nicht-Unix (Windows) oder in eingeschränkten Umgebungen, in denen keine Systemfunktionen zulässig sind.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/log
Verzeichnis 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:
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 ?
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:
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 ...
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 ).
quelle
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); }
quelle
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.
quelle
SplFileObject
Klasse, 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 .LimitIterator
ausgelöst wird,OutOfRangeException
wenn Ihre Datei weniger als 20 Zeilen enthältParameter offset must be >= 0
. Der zweite wirdLogicException
aus dem gleichen Grund werfen .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; }
quelle
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 ); }
quelle
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)); ?>
quelle
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));
quelle