web-dev-qa-db-de.com

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

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, die ich brauche, um die Anzahl der Zeilen für den Bereich von 2 MB bis 500 MB zu zählen. Vielleicht manchmal ein Gig.

Vielen Dank für jede Hilfe.

80
Abs

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 liest so lange 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 Zeit für die Wand 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 Zeichen am Zeilenende 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;
150
Dominic Rodger

Die Verwendung einer Schleife von fgets() Aufrufen ist eine gute Lösung und am einfachsten zu schreiben.

  1. auch wenn die Datei intern mit einem Puffer von 8192 Byte 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, werden Sie bereits mit dem Speichern von Funktionsaufrufen beginnen, und diese können sich summieren, wenn Sie große Dateien verarbeiten.

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, siehe hier was real bedeutet

101
Ja͢ck

Einfache orientierte Objektlösung

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

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

var_dump($file->key());

Aktualisieren

Ein anderer Weg, dies zu erreichen, ist mit PHP_INT_MAX im SplFileObject::seek Methode.

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

echo $file->key() + 1; 
41
Wallace Maxters

Wenn Sie dies auf einem Linux/Unix-Host ausführen, ist die einfachste Lösung die Verwendung von exec() oder einer ähnlichen Funktion, um den Befehl wc -l $path Auszuführen. Stellen Sie zunächst sicher, dass Sie $path Bereinigt haben, um sicherzustellen, dass es sich nicht um "/ path/to/file; rm -rf /" handelt.

34
Dave Sherohman

Ich habe festgestellt, dass es eine schnellere Methode gibt, bei der nicht die gesamte Datei durchlaufen werden muss

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

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

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

Wenn Sie PHP 5.5 verwenden, können Sie einen Generator verwenden. Dies wird [~ # ~] nicht [~ # ~ ] funktionieren in jeder Version von PHP vor 5.5. Von php.net:

"Generatoren bieten eine einfache Möglichkeit, einfache Iteratoren zu implementieren, ohne den Aufwand oder die Komplexität, eine 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
Ben Harold

Dies ist eine Ergänzung zu Wallace de Souza's

Beim Zählen werden auch 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; 
}
5
Jani

Wenn Sie unter Linux arbeiten, 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

4
elkolotfi

Das Zählen der Zeilenanzahl 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);
?>
1
Santosh Kumar
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 Verbesserung hinzufügen ...

in einem bestimmten Beispiel, in dem ich eine Datei hatte, die das Wort "testing" enthielt, gab die Funktion als Ergebnis 2 zurück. Also musste ich eine Prüfung hinzufügen, ob fgets falsch zurückgegeben hat oder nicht :)

habe Spaß :)

1
ufk

Es gibt eine andere Antwort, die ich für eine gute Ergänzung dieser Liste hielt.

Wenn Sie Perl installiert haben und in der Lage sind, Dinge in der Shell in PHP auszuführen:

$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 unter 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 bei der Flucht und Sie haben den Zugriff auf eine Shell auf Ihrem Computer übergeben.

Wie bei den meisten Dingen, die ich über das Codieren weiß (oder zu wissen glaube), habe ich diese Informationen von einer anderen Stelle erhalten:

John Reeve Article

0
Douglas.Sesar

Sie haben mehrere Möglichkeiten. Die erste Möglichkeit besteht darin, den verfügbaren Speicher zu erhöhen. Dies ist wahrscheinlich nicht die beste Möglichkeit, wenn Sie angeben, dass die Datei sehr groß werden kann. Die andere Möglichkeit besteht darin, fgets zu verwenden, um die Datei zeilenweise zu lesen und einen Zähler zu erhöhen. Dies sollte zu keinerlei Speicherproblemen führen, da sich zu jedem Zeitpunkt nur die aktuelle Zeile im Speicher befindet.

0
Yacoby
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>";
}
0
Yogi Sadhwani

Basierend auf der Lösung von dominic Rodger verwende ich Folgendes (es wird wc verwendet, sofern verfügbar, andernfalls wird auf die Lösung von dominic Rodger zurückgegriffen).

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

0
ling