Home
Navigation
Impressum
Coder Welten - Programmierung und Optimierung
Coder Welten
 
 

 

 

Auslesen (rückwärts) von umfangreichen CSV-Dateien

Klasse und Script mit Blätterfunktion

Übersicht » Einführung OOP

Beispiele:

  1. Parserklasse (eine simple Parserklasse für ein einfaches XML-Dokument)
  2. Socket Klasse (eine TCP/IP Socket Klasse zum Abruf wie durch einen Bot)
  3. Formular Klasse (Socket-Klasse mit einer Formular-Klasse kombinieren)
  4. cURL Klasse (eine cURL Session initialisieren und ausführen)
  5. CSV Klasse (zum Auslesen (rückwärts) von umfangreichen CSV-Dateien)

Aufgabenstellung und Vorbemerkungen

Eine CSV-Datei auszulesen und dabei die Ergebnisse zu sortieren, bereitet in der Regel keine größeren Probleme. Doch wie bei umfangreichen Dateien vorgehen, bei denen die letzten Zeilen zuerst ausgewertet werden sollen? Oder was tun, wenn die Laufzeit des Scripts zu einer für User unzumutbaren Ladezeit der Seiten führt oder gar der maximale Wert des Speichers überschritten wird, den ein Script ver­brauchen darf und nur eine Fehlermeldung wie die folgende erscheint?

Fatal error: Allowed memory size of 134217728 bytes exhausted ...

Im Folgenden wird ein Script beschrieben, welches nur die Zeilen aus einer CSV-Datei einliest und in einem Array speichert, die für die Auswertung und Ausgabe benötigt werden und dabei mit den letzten Zeilen der jeweiligen CSV-Datei beginnt. Es sei angemerkt, dass es einfachere und schnellere Methoden gibt, insofern die Dateien beim Einlesen nicht den maximalen Speicherwert überschreiten und das Memory Limit nicht hochgesetzt werden soll oder hochgesetzt werden kann.

Beschreibung des Scripts

Für die Tests wurde eine CSV-Dateien mit 200.000 Zeilen und mit einer Gesamt­größe von 181 MB verwendet, wobei jede Zeile der Testdatei 6 Felder enthielt. Um die Blätterfunktion zu nutzen oder die Anzahl der pro Seite auszugebenen Ergebnisse zu verändern, kann der URI ein QueryString hinzugefügt werden, der je ein Parameter-Werte-Paar für die aufzurufende Seite und die Anzahl der Ergebnisse pro Seite enthalten kann, jedoch nicht zwingend muss.

Beispiel:

"www.example.com/csv.php?seite=1&anzahl=30"
"www.example.com/csv.php?seite=2&anzahl=30"

Im Beispiel würden bei einer CSV-Dateien mit 200.000 die Ergebnisse wie folgt ausgegeben:

Seite1: 200.000 bis 199.971
Seite2: 199.970 bis 199.941

Die Links zum Blättern (rückwärts wie vorwärts) werden vom Script generiert. Die Anzahl der auszugebenen Ergebnisse müssen hingegen in dieser einfachen Version, die nur als Demo für durchgeführte Tests diente, noch vorgewählt werden. Soll die Auswahl durch Besuchereingaben erfolgen, könnte dem Script noch ein kleines Formular mit Radio-Buttons hinzugefügt werden. Ein Beispiel für ein Formular finden Sie weiter unten auf dieser Seite. Die Ausgabe der Zeilen erfolgt unformatiert und kann beliebig dem Verwendungszweck entsprechend angepasst werden.

Einzelheiten

Die Anzahl der Zeilen der jeweiligen CSV-Datei wird mit der ersten While-Schleife und fgets ermittelt. Da diese Schleife keine weiteren Aufgaben zu erfüllen hat, muss nach Beendigung des kompletten Durchlaufs der Dateizeiger mit der Funktion rewind wieder auf den Anfang des Dokumentes zurückgesetzt werden.

Die zweite Schleife beginnt somit wieder bei der ersten Zeile der Datei mit dem Auszählen. Die Aufgabe dieser zweiten Schleife dient lediglich dem Hochzählen bis zur jeweiligen Anfangsposition. Zum Beispiel bis Zeile 199.970 bei Seite 1 oder 199.940 bei Seite 2, falls die CSV 200.000 Zeilen hat und 30 Ergebnisse pro Seite ausgelesen und ausgegeben werden sollen. Erst die innere While-Schleife beginnt im Zusammenspiel mit der Funktion fgetcsv die entsprechenden Zeilen einzulesen und in einem Array zu speichern. Der Vorteil besteht darin, dass nie mehr als die gewünschte Anzahl an auszugebenen Ergebnissen in dem Array eingespeist und in diesem gespeichert Array werden.
Da die äußere While jedoch mindestens bis 1 zählt, muss diese ebenfalls wieder mit rewind zurückgesetzt werden, wenn die Zeilen 1 bis 30 (oder bis 10, 20, 50, oder bis wohin auch immer) eingelesen werden sollen, jedoch nur in diesem speziellen Fall. Um die gespeicherten Zeilen in der richtigen Reihenfolge auszugeben und mit den jeweils letzten Zeilen zu beginnen (um ein Rückwärtslesen konsequent umzusetzen), muss die Reihenfolge der Zeilen im Array abschließend noch mit array_reverse vertauscht werden.

Code-Listing/Script (csv-rueckwaertslesen.php):

<?php
error_reporting(E_ALL);

$csvdat = "test.csv";         // Die auszuwertende Datei angeben

class CsvInputOutput {

    private $sepa  = ";";      // Trennzeichen, Separator
    private $whi   =   0;      // keine Änderung erforderlich
    private $abz   =   0;      // keine Änderung erforderlich
    public  $rows  =   0;      // keine Änderung erforderlich
    public  $seite =   1;      // Start beginnt mit Seite 1, falls nichts anderes übergeben wurde
    public  $ergb  =  20;      // Anzahl der Ergebnisse pro Seite, falls nichts anderes übergeben wurde
    private $leng  =   0;      // kann bei CSV-Dateien ohne überlange Zeilen auf 4096 oder weniger gesetzt werden
    private $daten = array();  // Array für die aufzunehmenden Zeilen und Felder
    public  $csv;

/* -- Falls ein QueryString mit anzahl=wert und oder mit seite=wert übergeben wurde -- */

    public function setSeite() {

        if (isset($_GET["seite"]) and !empty($_GET["seite"])) {

            $seite = trim($_GET["seite"]);
            $seite = preg_replace("/[^0-9]/", "", $seite);

            $this->seite = $seite;
            return $this->seite;
        }
    }
    public function setAnzahl() {

        if (isset($_GET["anzahl"]) and !empty($_GET["anzahl"])) {

            $anzahl = trim($_GET["anzahl"]);
            $anzahl = preg_replace("/[^0-9]/", "", $anzahl);

            $this->ergb = $anzahl;
            return $this->ergb;
        }
    }

/* -- Die eigentliche Methode getCSV ------------------------------------------------- */

    public function getCSV() {

        $asg2 = $this->ergb *$this->seite;

        if (($handle = fopen($this->csv, "r")) !== false) {

            while (fgets($handle) !== false ) {

                $this->rows++;
            }
            $abzeile = $this->rows - $asg2;
            rewind($handle);

            if ($abzeile < 0) {
                $restli  = $abzeile + $this->ergb;
                $abzeile = 0;
            }
            else {$restli  = $this->ergb;
            }

            while (fgets($handle) !== false) {

                $this->abz++;

                if ($this->abz >= $abzeile and $abzeile >= 0) {

                    if ($abzeile == 0 and $this->abz == 1) {
                        rewind($handle);
                    }
                    while (($data = fgetcsv($handle, $this->leng, $this->sepa)) !== false) {

                        if ($this->whi < $restli){

                            $this->daten[] = $data;
                        }
                        $this->whi++;
                    }
                }
            }
            fclose($handle);

            if (count($this->daten) !== 0) {
                $this->daten = array_reverse($this->daten);
            }
            else {
                $this->daten = array(0 => array("Keine Daten verfügbar!"));
            }
            return $this->daten;
        }
        else {
            return $daten = array(0 => array("Fehler, die Datei ".
            $this->csv." konnte nicht geladen werden!"));
        }
    }
}

/* -- Nur eine Blätterfunktion (eigentlich eine Klasse mit einer Methode) ------------ */

class BlaetterFunktion {

    public $fpage;    // Seite
    public $fergeb;   // Ergebnisse
    public $fline;    // Zeilen

    public function getBlaetterSeite() {

        $blatt = "";

        if ($this->fpage > 1) {

            $blatt .= "<a href=\"".htmlspecialchars("?".
                      "seite=" .($this->fpage -1)."&".
                      "anzahl=". $this->fergeb).
                      "\">« zur&uuml;ck </a>";
        }
        $blatt .= " | ";

        if (($this->fline /($this->fpage *$this->fergeb)) >= 1) {

            $blatt .= "<a href=\"".htmlspecialchars("?".
                      "seite=" .($this->fpage +1)."&".
                      "anzahl=". $this->fergeb).
                      "\">weiter »</a>\n";
        }
        return $blatt;
    }
}

$csvobj = new CsvInputOutput();    // Erzeugen und Instanziieren des Objektes CsvInputOutput
$csvobj->setSeite();               // Rückgabewert der Methode setSeite $seite zuweisen
$csvobj->setAnzahl();              // Rückgabewert der Methode setAnzahl $ergb zuweisen
$csvobj->csv = $csvdat;            // Die angegebene Datei $csv zuweisen
$csvarr = $csvobj->getCSV();       // Der zurückgegebene Array
$couarr = count($csvarr);          // Ergebnisse für die erste for-Schleife zählen
$numarr = count($csvarr[0]);       // Felder der CSV für die zweite for-Schleife zählen
$ergebn = $csvobj->ergb;           // Anzahl der Ergebnisse für die erste for-Schleife

$blatt = new BlaetterFunktion();   // Nur zum Weiterblättern
$blatt->fpage  = $csvobj->seite;   // Seite 1, 2, 3
$blatt->fergeb = $csvobj->ergb;    // Ergebnisse pro Seite zum Beispiel 20 oder 30
$blatt->fline  = $csvobj->rows;    // Zeilen zum Beispiel 200.000

?>
<!DOCTYPE html>
<html>

<head>
<title>CSV-Dateien rückwärts auslesen</title>
</head>

<body>
<h1 style="text-align:center">CSV-Dateien rückwärts lesen</h1>
<?php

/* -- Die Ausgabe kann nach Belieben dem Verwendungszweck angepasst werden ----------- */

echo "<ol>\n";

for ($i = 0; $i < $couarr; $i++) {

    echo "<li>\n<ul>\n";

    for ($n = 0; $n < $numarr; $n++) {

        echo "<li>".htmlspecialchars($csvarr[$i][$n])."</li>";
    }
    echo "</ul>\n</li>\n";
}
echo "</ol>\n<p style=\"text-align:center\">".$blatt->getBlaetterSeite()."</p>\n";

?>
</body>
</html>


Bemerkungen zur Laufzeit und Ladezeit

Ergänzend noch einige Bemerkungen zur Laufzeit des Scripts und zur Ladezeit der Seiten. Bei den durchgeführten Tests wurden sowohl die Laufzeit des Scripts mit microtime, als auch die Ladezeit der Seiten beim Blättern mit Firebug getestet. Die angegeben Werte sind nur Durchschnittswerte, die sich von Seite zu Seite geringfügig unterscheiden.

Ladezeit der Seiten unter Localhost
Blättern von einer Seite zur anderen


Größe der CSV-Datei 181 MB mit 200.000 Zeilen
Bei 20 auszugebenen Ergebnissen pro Seite: 561ms (onload: 627ms)

Größe der CSV 16,2 MB mit 18.000 Zeilen
Bei 20 auszugebenen Ergebnissen pro Seite: 110ms (onload: 166ms)
Bei 50 auszugebenen Ergebnissen pro Seite: 140ms (onload: 207ms)

Ladezeit der Seiten im Web
Blättern von einer Seite zur anderen


Größe der CSV 16,2 MB mit 18.000 Zeilen
Bei 20 auszugebenen Ergebnissen pro Seite: 561ms (onload: 630ms)

Abschließend sei bemerkt, dass vorgestellte Script hält keinen Vergleich mit einer Datenbank stand und wurde nur für Testzwecke gefertigt. Wenn immer die Möglichkeit besteht, ist hingegen der gut beraten, der Daten aus CSV-Dateien in eine MySQL-Datenbank abspeichert und je nach Verwendungszweck, regelmäßig updatet.

 

Copyright © Verlag Horst Müller - Stendal - 2006 - Impressum - Datenschutz - Nutzungsbedingungen