Frage Zählt effizient die Anzahl der Zeilen einer Textdatei. (200mb +)


Ich habe gerade herausgefunden, dass mein Skript einen fatalen Fehler verursacht:

Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 440 bytes) in C:\process_txt.php on line 109

Diese Zeile ist das:

$lines = count(file($path)) - 1;

Ich denke, es ist schwierig, die Datei in den Speicher zu laden und die Anzahl der Zeilen zu zählen. Gibt es einen effizienteren Weg, dies ohne Speicherprobleme zu tun?

Die Textdateien, die ich zählen muss die Anzahl der Zeilen für den Bereich von 2 MB bis 500 MB. Vielleicht ein Gig manchmal.

Danke an alle für jede Hilfe.


74
2018-01-29 14:26


Ursprung


Antworten:


Dadurch wird weniger Speicher benötigt, da nicht die gesamte Datei in den Speicher geladen wird:

$file="largefile.txt";
$linecount = 0;
$handle = fopen($file, "r");
while(!feof($handle)){
  $line = fgets($handle);
  $linecount++;
}

fclose($handle);

echo $linecount;

fgets lädt eine einzelne Zeile in den Speicher (wenn das zweite Argument $length wird weggelassen, es wird weiterlesen aus dem Stream, bis es das Ende der Zeile erreicht, was wir wollen). Dies ist immer noch nicht so schnell wie mit etwas anderem als PHP, wenn Sie sowohl an der Wandzeit als auch an der Speicherauslastung interessiert sind.

Die einzige Gefahr besteht darin, dass Zeilen besonders lang sind (was ist, wenn Sie eine 2-GB-Datei ohne Zeilenumbrüche finden?). In diesem Fall ist es besser, wenn Sie es in Blöcken schlürfen und End-of-Line-Zeichen zählen:

$file="largefile.txt";
$linecount = 0;
$handle = fopen($file, "r");
while(!feof($handle)){
  $line = fgets($handle, 4096);
  $linecount = $linecount + substr_count($line, PHP_EOL);
}

fclose($handle);

echo $linecount;

140
2018-01-29 14:31



Mit einer Schleife von fgets() Anrufe sind eine feine Lösung und am einfachsten zu schreiben, jedoch:

  1. Obwohl intern die Datei mit einem Puffer von 8192 Byte gelesen wird, muss der Code diese Funktion für jede Zeile aufrufen.

  2. Es ist technisch möglich, dass eine einzelne Zeile größer als der verfügbare Speicher ist, wenn Sie eine Binärdatei lesen.

Dieser Code liest eine Datei in Blöcken von jeweils 8 KB und zählt dann die Anzahl der Zeilenumbrüche innerhalb dieses Blocks.

function getLines($file)
{
    $f = fopen($file, 'rb');
    $lines = 0;

    while (!feof($f)) {
        $lines += substr_count(fread($f, 8192), "\n");
    }

    fclose($f);

    return $lines;
}

Wenn die durchschnittliche Länge jeder Zeile höchstens 4 KB beträgt, werden Sie bereits bei Funktionsaufrufen gespeichert, und diese können sich bei der Verarbeitung großer Dateien addieren.

Benchmark

Ich habe einen Test mit einer 1 GB Datei durchgeführt; Hier sind die Ergebnisse:

             +-------------+------------------+---------+
             | This answer | Dominic's answer | wc -l   |
+------------+-------------+------------------+---------+
| Lines      | 3550388     | 3550389          | 3550388 |
+------------+-------------+------------------+---------+
| Runtime    | 1.055       | 4.297            | 0.587   |
+------------+-------------+------------------+---------+

Die Zeit wird in Sekunden Echtzeit gemessen, siehe Hier was wirklich bedeutet


97
2017-12-12 07:08



Einfach orientierte Objektlösung

$file = new \SplFileObject('file.extension');

while($file->valid()) $file->fgets();

var_dump($file->key());

Aktualisieren

Eine andere Möglichkeit dies zu machen ist mit PHP_INT_MAX im SplFileObject::seek Methode.

$file = new \SplFileObject('file.extension', 'r');
$file->seek(PHP_INT_MAX);

echo $file->key() + 1; 

35
2017-07-24 13:18



Wenn Sie dies auf einem Linux / Unix-Host ausführen, wäre die einfachste Lösung die Verwendung exec() oder ähnlich, um den Befehl auszuführen wc -l $path. Stellen Sie nur sicher, dass Sie gereinigt haben $path um sicher zu sein, dass es nicht etwa "/ path / to / file; rm -rf /" ist.


33
2018-01-29 14:30



Es gibt einen schnelleren Weg, den ich gefunden habe, der nicht die gesamte Datei durchlaufen muss

nur auf * nix Systemen, könnte es einen ähnlichen Weg auf Windows geben ...

$file = '/path/to/your.file';

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));

25
2018-03-17 21:18



Wenn Sie PHP 5.5 verwenden, können Sie a Generator. Dieser Wille NICHT funktioniert in jeder Version von PHP vor Version 5.5. Von php.net:

"Generatoren bieten eine einfache Möglichkeit, einfache Iteratoren ohne den Aufwand oder die Komplexität der Implementierung einer Klasse zu implementieren, die die Iterator-Schnittstelle implementiert."

// This function implements a generator to load individual lines of a large file
function getLines($file) {
    $f = fopen($file, 'r');

    // read each line of the file without loading the whole file to memory
    while ($line = fgets($f)) {
        yield $line;
    }
}

// Since generators implement simple iterators, I can quickly count the number
// of lines using the iterator_count() function.
$file = '/path/to/file.txt';
$lineCount = iterator_count(getLines($file)); // the number of lines in the file

8
2017-10-12 01:53



Dies ist eine Ergänzung zu Wallace de Souzas Lösung

Es überspringt auch leere Zeilen beim Zählen:

function getLines($file)
{
    $file = new \SplFileObject($file, 'r');
    $file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY | 
SplFileObject::DROP_NEW_LINE);
    $file->seek(PHP_INT_MAX);

    return $file->key() + 1; 
}

4
2018-06-28 07:09