Einleitung: Wenn normales file_get_contents den Server lahmlegt
\nStellen Sie sich vor: Sie schreiben einen Log-Parser, einen CSV-Import mit Millionen von Zeilen oder verarbeiten einen Datenbank-Dump. Sie schreiben $data = file_get_contents('bigfile.log'); – und Ihr PHP-Skript stürzt mit dem Fehler Fatal error: Allowed memory size exhausted ab. Kommt Ihnen bekannt vor? Wenn nicht, haben Sie Glück gehabt. Aber früher oder später steht jeder Entwickler vor der Aufgabe: „Wie lese ich eine große Datei zeilenweise, ohne den Server zu killen?“.
Auf Stack Overflow hat die Frage „How to read a large file line by line?“ Hunderttausende von Aufrufen. Und das ist kein Zufall. In diesem Artikel behandeln wir nicht nur eine Methode, sondern mehrere Ansätze – vom Basis- bis zum Fortgeschrittenen-Level. Sie erfahren, wie Sie Dateien mit einer Größe von Dutzenden Gigabyte lesen können, indem Sie nur ein paar Megabyte Arbeitsspeicher verwenden. Los geht's!
\n\nWarum file_get_contents() und file() für große Dateien schlecht sind
\nBevor wir in die Optimierung eintauchen, lassen Sie uns verstehen, warum die Standardfunktionen ungeeignet sind.
\n- \n
- file_get_contents() – lädt die gesamte Datei in einen String. Wenn die Datei 2 GB wiegt, versucht PHP, 2 GB RAM zu reservieren. Auf einem Hosting mit einem memory_limit von 128 MB ist das ein garantierter Fatal Error. \n
- file() – liest ebenfalls die gesamte Datei, zerlegt sie aber zusätzlich in ein Array von Zeilen. Der Speicherverbrauch steigt noch weiter an (plus Overhead für das Array). \n
Die Moral: Wenn die Datei größer als 10-20% Ihres memory_limit ist, vergessen Sie diese Funktionen. Wir brauchen einen streaming-Ansatz.
\n\nMethode 1: Klassiker – fgets() und Datei-Handle
\nDie grundlegendste und zuverlässigste Methode ist, die Datei mit fopen() zu öffnen und sie zeilenweise mit fgets() zu lesen. Dieser Ansatz verwendet den internen PHP-Puffer, der normalerweise 8 KB beträgt, und lädt nicht die gesamte Datei in den Speicher.
<?php\n$handle = fopen('bigfile.log', 'r');\nif ($handle) {\n while (($line = fgets($handle)) !== false) {\n // Verarbeite die Zeile\n echo $line; // oder in DB schreiben, oder etwas zählen\n }\n fclose($handle);\n} else {\n // Fehler beim Öffnen der Datei\n echo "Datei konnte nicht geöffnet werden.";\n}\n?>\n\nVorteile: minimaler Speicherverbrauch, funktioniert mit jedem PHP.
\nNachteile: Datei muss explizit geschlossen werden, keine praktischen Methoden zum Überspringen von Kopfzeilen oder zum Iterieren mit Zeilenzählung.
\nDieser Code überlebt Dateien jeder Größe, solange Sie nicht versuchen, alle Zeilen innerhalb der Schleife in einem Array zu speichern.
\n\nMethode 2: SplFileObject – objektorientierter Ansatz
\nSeit PHP 5.1 haben wir die hervorragende Klasse SplFileObject. Sie implementiert die Interfaces Iterator und SeekableIterator, was sie ideal für zeilenweises Lesen macht.
<?php\n$file = new SplFileObject('bigfile.log');\nwhile (!$file->eof()) {\n $line = $file->fgets();\n // Verarbeite die Zeile\n echo $line;\n}\n?>\n\nAber es gibt einen Haken! SplFileObject kann standardmäßig ebenfalls Zeilen in den Speicher laden, wenn Sie current() innerhalb einer foreach-Schleife verwenden. Um einen geringen Verbrauch zu garantieren, verwenden Sie fgets() in einer while-Schleife, wie oben gezeigt.
Eine fortgeschrittenere Variante ist das Zurücksetzen von Flags zur Optimierung:
\n\n<?php\n$file = new SplFileObject('bigfile.log');\n$file->setFlags(SplFileObject::DROP_NEW_LINE); // Entfernt \\n aus der Zeile\n\nforeach ($file as $lineNum => $line) {\n // $lineNum – Zeilennummer (beginnend bei 0)\n echo "Zeile $lineNum: $line\\n";\n}\n?>\n\nWichtig: Bei Verwendung von foreach mit SplFileObject liest PHP dennoch jeweils eine Zeile auf einmal. Der Speicher leidet nicht. Aber wenn Sie versuchen, $file->seek(1000000) aufzurufen und dann in der Datei zu springen – seien Sie vorsichtig: seek durchläuft die