Effizientes Zählen der Zeilenanzahl einer Textdatei. (200 MB +)

88

Ich habe gerade herausgefunden, dass mein Skript einen schwerwiegenden 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 lautet:

$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 eine effizientere Möglichkeit, dies ohne Speicherprobleme zu tun?

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

Vielen Dank für jede Hilfe.

Abs
quelle

Antworten:

158

Dies verbraucht weniger Speicher, 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;

fgetsLädt eine einzelne Zeile in den Speicher (wenn das zweite Argument $lengthweggelassen wird, liest es weiter aus dem Stream, bis es das Ende der Zeile erreicht, was wir wollen). Es ist immer noch unwahrscheinlich, dass dies so schnell geht wie die Verwendung von etwas anderem als PHP, wenn Sie sowohl die Wandzeit als auch die Speichernutzung berücksichtigen.

Die einzige Gefahr besteht darin, dass Zeilen besonders lang sind (was ist, wenn Sie auf eine 2-GB-Datei ohne Zeilenumbrüche stoßen?). In diesem Fall ist es besser, wenn Sie es in Stücken schlürfen und die Zeilenendezeichen 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;
Dominic Rodger
quelle
5
nicht perfekt: Sie könnten eine Datei im Unix-Stil ( \n) haben, die auf einem Windows-Computer ( PHP_EOL == '\r\n')
analysiert wird
1
Warum nicht ein bisschen verbessern, indem Sie den Zeilenwert auf 1 beschränken? Da wir nur die Anzahl der Zeilen zählen möchten, warum nicht eine fgets($handle, 1);?
Cyril N.
1
@ CyrilN. Dies hängt von Ihrem Setup ab. Wenn Sie hauptsächlich Dateien haben, die nur einige Zeichen pro Zeile enthalten, kann dies schneller sein, da Sie diese nicht verwenden müssen. substr_count()Wenn Sie jedoch sehr lange Zeilen haben, müssen Sie anrufen while()und fgets()vieles mehr, was einen Nachteil verursacht. Vergessen Sie nicht: fgets() liest nicht Zeile für Zeile. Es liest nur die Anzahl der Zeichen, die Sie definiert haben, $lengthund wenn es einen Zeilenumbruch enthält, stoppt es alles $length, was eingestellt wurde.
mgutt
3
Wird dies nicht 1 mehr als die Anzahl der Zeilen zurückgeben? while(!feof())Dies führt dazu, dass Sie eine zusätzliche Zeile lesen, da der EOF-Indikator erst gesetzt wird, nachdem Sie versucht haben, am Ende der Datei zu lesen.
Barmar
1
@DominicRodger im ersten Beispiel könnte meiner Meinung $line = fgets($handle);nach nur sein, fgets($handle);weil $linees nie verwendet wird.
Taschen und
106

Die Verwendung einer Anrufschleife fgets()ist eine gute Lösung und am einfachsten zu schreiben:

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

  2. Es ist technisch möglich, dass eine einzelne Zeile größer ist als der verfügbare Speicher, 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 in diesem Block.

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, sparen Sie bereits bei Funktionsaufrufen. Diese können sich bei der Verarbeitung großer Dateien summieren.

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 in Echtzeit gemessen. Sehen Sie hier, was Real bedeutet

Jack
quelle
Neugierig, wie schnell (?) Es sein wird, wenn Sie die Puffergröße auf etwa 64 KB erweitern. PS: Wenn nur PHP eine einfache Möglichkeit hätte, IO in diesem Fall asynchron zu machen
Zerkms
@zerkms Um Ihre Frage zu beantworten, mit 64kB Puffern wird es 0,2 Sekunden schneller auf 1 GB :)
Ja͢ck
3
Seien Sie vorsichtig mit diesem Benchmark, den Sie zuerst ausgeführt haben? Die zweite hat den Vorteil, dass sich die Datei bereits im Festplatten-Cache befindet und das Ergebnis massiv verzerrt.
Oliver Charlesworth
6
@OliCharlesworth sie sind durchschnittlich über fünf Läufe, überspringen den ersten Lauf :)
Ja͢ck
1
Diese Antwort ist großartig! Allerdings IMO, muss sie prüfen , wenn es einige Zeichen in der letzten Zeile 1 in der Zeilenzahl hinzuzufügen: pastebin.com/yLwZqPR2
caligari
46

Einfache orientierte Objektlösung

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

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

var_dump($file->key());

Aktualisieren

Ein anderer Weg , dies zu machen , ist mit PHP_INT_MAXin - SplFileObject::seekVerfahren.

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

echo $file->key() + 1; 
Wallace Maxters
quelle
3
Die zweite Lösung ist großartig und verwendet Spl! Vielen Dank.
Daniele Orlando
2
Danke ! Das ist in der Tat großartig. Und schneller als das Aufrufen wc -l(wegen der Verzweigung, nehme ich an), besonders bei kleinen Dateien.
Drasill
Ich hätte nicht gedacht, dass die Lösung so hilfreich sein würde!
Wallace Maxters
2
Dies ist bei weitem die beste Lösung
Valdrinium
1
Ist die "Taste () + 1" richtig? Ich habe es versucht und scheint falsch. Für eine bestimmte Datei mit Zeilenenden in jeder Zeile, einschließlich der letzten, gibt mir dieser Code 3998. Wenn ich jedoch "wc" mache, erhalte ich 3997. Wenn ich "vim" verwende, wird 3997L angezeigt (und es wird nicht angezeigt, dass es fehlt EOL). Daher denke ich, dass die Antwort "Update" falsch ist.
user9645
37

Wenn Sie dies auf einem Linux / Unix-Host exec()ausführen , ist es am einfachsten , den Befehl zu verwenden oder ähnlich wc -l $path. Stellen Sie $patheinfach sicher, dass Sie zuerst bereinigt haben , um sicherzustellen, dass es sich nicht um "/ path / to / file; rm -rf /" handelt.

Dave Sherohman
quelle
Ich bin auf einer Windows-Maschine! Wenn ich es wäre, wäre das die beste Lösung!
Abs
22
@ Ghostdog74: Ja, du hast recht. Es ist nicht tragbar. Aus diesem Grund habe ich die Nichtportabilität meines Vorschlags ausdrücklich anerkannt, indem ich ihm die Klausel "Wenn Sie dies auf einem Linux / Unix-Host ausführen ..." vorangestellt habe.
Dave Sherohman
1
Nicht portabel (obwohl in einigen Situationen nützlich), aber exec (oder shell_exec oder system) sind Systemaufrufe, die im Vergleich zu in PHP integrierten Funktionen erheblich langsamer sind.
Manz
10
@Manz: Ja, du hast recht. Es ist nicht tragbar. Aus diesem Grund habe ich die Nichtportabilität meines Vorschlags ausdrücklich anerkannt, indem ich ihm die Klausel "Wenn Sie dies auf einem Linux / Unix-Host ausführen ..." vorangestellt habe.
Dave Sherohman
@ DaveSherohman Ja, du hast recht, sorry. IMHO, ich denke, das wichtigste Problem ist der zeitaufwändige in einem Systemaufruf (vor allem, wenn Sie häufig verwenden müssen)
Manz
31

Ich habe einen schnelleren Weg gefunden, bei dem nicht die gesamte Datei durchlaufen werden muss

Nur auf * nix-Systemen kann es unter Windows einen ähnlichen Weg geben ...

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

//Get number of lines
$totalLines = intval(exec("wc -l '$file'"));
Andy Braham
quelle
füge 2> / dev / null hinzu, um das "Keine solche Datei oder Verzeichnis" zu unterdrücken
Tegan Snyder
$ total_lines = intval (exec ("wc -l '$ file'")); behandelt Dateinamen mit Leerzeichen.
pgee70
Danke pgee70 ist noch nicht darauf gestoßen, aber es macht Sinn, ich habe meine Antwort aktualisiert
Andy Braham
6
exec('wc -l '.escapeshellarg($file).' 2>/dev/null')
Zheng Kai
Sieht aus wie die Antwort von @DaveSherohman oben 3 Jahre vor dieser
e2-e4
8

Wenn Sie PHP 5.5 verwenden, können Sie einen Generator verwenden . Dies wird nicht in jeder Version von PHP arbeiten , bevor 5.5 though. Von php.net:

"Generatoren bieten eine einfache Möglichkeit, einfache Iteratoren zu implementieren, ohne den Aufwand oder die Komplexität der Implementierung einer Klasse, 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
Ben Harold
quelle
5
Das try/ finallyist nicht unbedingt erforderlich, PHP schließt die Datei automatisch für Sie. Sie sollten wahrscheinlich auch erwähnen, dass die eigentliche Zählung mit iterator_count(getFiles($file)):)
NikiC
6

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

Außerdem werden beim Zählen leere Zeilen übersprungen:

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; 
}
Jani
quelle
6

Wenn Sie unter Linux sind, können Sie einfach Folgendes tun:

number_of_lines = intval(trim(shell_exec("wc -l ".$file_name." | awk '{print $1}'")));

Sie müssen nur den richtigen Befehl finden, wenn Sie ein anderes Betriebssystem verwenden

Grüße

elkolotfi
quelle
1
private static function lineCount($file) {
    $linecount = 0;
    $handle = fopen($file, "r");
    while(!feof($handle)){
        if (fgets($handle) !== false) {
                $linecount++;
        }
    }
    fclose($handle);
    return  $linecount;     
}

Ich wollte der obigen Funktion eine kleine Korrektur hinzufügen ...

In einem speziellen Beispiel, in dem ich eine Datei hatte, die das Wort 'Testen' enthielt, gab die Funktion als Ergebnis 2 zurück. Also musste ich eine Überprüfung hinzufügen, ob Fgets falsch zurückgegeben wurden oder nicht :)

habe Spaß :)

ufk
quelle
1

Das Zählen der Anzahl der Zeilen kann mit folgenden Codes erfolgen:

<?php
$fp= fopen("myfile.txt", "r");
$count=0;
while($line = fgetss($fp)) // fgetss() is used to get a line from a file ignoring html tags
$count++;
echo "Total number of lines  are ".$count;
fclose($fp);
?>
Santosh Kumar
quelle
0

Sie haben mehrere Möglichkeiten. Die erste besteht darin, den verfügbaren verfügbaren Speicher zu erhöhen. Dies ist wahrscheinlich nicht der beste Weg, um Dinge zu tun, da Sie angeben, dass die Datei sehr groß werden kann. Die andere Möglichkeit besteht darin, fgets zu verwenden, um die Datei Zeile für Zeile zu lesen und einen Zähler zu erhöhen , was überhaupt keine Speicherprobleme verursachen sollte, da sich jeweils nur die aktuelle Zeile im Speicher befindet.

Yacoby
quelle
0

Es gibt noch eine andere Antwort, von der ich dachte, dass sie eine gute Ergänzung zu dieser Liste sein könnte.

Wenn Sie perlin PHP Dinge von der Shell installiert haben und ausführen können:

$lines = exec('perl -pe \'s/\r\n|\n|\r/\n/g\' ' . escapeshellarg('largetextfile.txt') . ' | wc -l');

Dies sollte die meisten Zeilenumbrüche behandeln, unabhängig davon, ob es sich um von Unix oder Windows erstellte Dateien handelt.

ZWEI Nachteile (mindestens):

1) Es ist keine gute Idee, Ihr Skript so abhängig von dem System zu machen, auf dem es ausgeführt wird (es ist möglicherweise nicht sicher anzunehmen, dass Perl und wc verfügbar sind).

2) Nur ein kleiner Fehler beim Entkommen und Sie haben den Zugriff auf eine Shell auf Ihrer Maschine übergeben.

Wie bei den meisten Dingen, die ich über Codierung weiß (oder zu wissen glaube), habe ich diese Informationen von einem anderen Ort erhalten:

John Reeve Artikel

Douglas.Sesar
quelle
0
public function quickAndDirtyLineCounter()
{
    echo "<table>";
    $folders = ['C:\wamp\www\qa\abcfolder\',
    ];
    foreach ($folders as $folder) {
        $files = scandir($folder);
        foreach ($files as $file) {
            if($file == '.' || $file == '..' || !file_exists($folder.'\\'.$file)){
                continue;
            }
                $handle = fopen($folder.'/'.$file, "r");
                $linecount = 0;
                while(!feof($handle)){
                    if(is_bool($handle)){break;}
                    $line = fgets($handle);
                    $linecount++;
                  }
                fclose($handle);
                echo "<tr><td>" . $folder . "</td><td>" . $file . "</td><td>" . $linecount . "</td></tr>";
            }
        }
        echo "</table>";
}
Yogi Sadhwani
quelle
5
Bitte denken Sie daran, mindestens einige Wörter hinzuzufügen, die dem OP erklären, und weiteren Lesern von Ihnen zu antworten, warum und wie es auf die ursprüngliche Frage antwortet.
β.εηοιτ.βε
0

Basierend auf der Lösung von Dominic Rodger verwende ich Folgendes (es verwendet wc, falls verfügbar, andernfalls greift es auf die Lösung von Dominic Rodger zurück).

class FileTool
{

    public static function getNbLines($file)
    {
        $linecount = 0;

        $m = exec('which wc');
        if ('' !== $m) {
            $cmd = 'wc -l < "' . str_replace('"', '\\"', $file) . '"';
            $n = exec($cmd);
            return (int)$n + 1;
        }


        $handle = fopen($file, "r");
        while (!feof($handle)) {
            $line = fgets($handle);
            $linecount++;
        }
        fclose($handle);
        return $linecount;
    }
}

https://github.com/lingtalfi/Bat/blob/master/FileTool.php

ling
quelle
0

Ich benutze diese Methode, um nur zu zählen, wie viele Zeilen in einer Datei enthalten sind. Was ist der Nachteil dieses Verses gegenüber den anderen Antworten? Ich sehe viele Zeilen im Gegensatz zu meiner zweizeiligen Lösung. Ich vermute, es gibt einen Grund, warum niemand dies tut.

$lines = count(file('your.file'));
echo $lines;
kaspirtk1
quelle
Die ursprüngliche Lösung war dies. Da file () die gesamte Datei in den Speicher lädt, war dies auch das ursprüngliche Problem (Speicherauslastung). Nein, dies ist keine Lösung für die Frage.
Tuim
0

Die prägnanteste plattformübergreifende Lösung, die jeweils nur eine Zeile puffert.

$file = new \SplFileObject(__FILE__);
$file->setFlags($file::READ_AHEAD);
$lines = iterator_count($file);

Leider müssen wir das READ_AHEADFlag setzen, sonst iterator_countblockiert es auf unbestimmte Zeit. Andernfalls wäre dies ein Einzeiler.

Quolonel Fragen
quelle
-1

Um nur die Zeilen zu zählen, verwenden Sie:

$handle = fopen("file","r");
static $b = 0;
while($a = fgets($handle)) {
    $b++;
}
echo $b;
Adeel Ahmad
quelle