Fabian St.: Nur einen Teil einer Datei auslesen

Hi!

Ich stehe im Moment vor dem Problem, aus einer .csv-Datei nur einen Teil auslesen zu müssen. Konkret bedeutet dies z.B., dass folgender Aufbau gegeben sei:

id | vname | nname | nachricht

Jetzt sollen immer nur diejenigen Datensätze angezeigt werden, die von der gleichen Person geschrieben wurden. In SQL würde es ja über die WHERE-Klausel funktionieren, aber bei Dateien habe ich bisher keine andere Möglichkeit gefunden, als die ganze Datei in ein Array einzulesen und dieses dann zu untersuchen.

Meinen bisherigen Versuch könnte hier sehen: http://fabis-site.net/self/show.php?file=split.php

Meine Frage ist daher, ob es vielleicht eine ressourcenschonendere Möglichkeit gibt, die ähnlich einfach umzusetzen ist oder ihr irgendwelche Verbesserungsvorschläge bzgl. des obigen Stück Codes habt.

Grüße,
Fabian St.

P.s. Ein Live-Beispiel gibts hier: http://fabis-site.net/self/split.html - Eingabeformular (Daten werden per POST an split.php gesendet, wo dann die Anzeige der Datensätze stattfindet und die Selektion vorgenommen werden kann.

--
Meine Website: http://fabis-site.net
--> XHTML, CSS, PHP-Formmailer, Linux
---------------------
fabi@erde ~# whatis spam
spam: nothing appropriate
---------------------
Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
  1. Hello,

    es hängt immer von der Art der Speicherung ab.

    Wenn Datensätze als geschlossene Gruppen angereiht (egal ob mit wahlfreiem Zugriff oder mit sequentiellem) abgespeichert werden, hast Du keine andere Wahl, als die ganze Datei "durchzuziehen". Man kann das Satz für Satz oder Bereich für Bereich machen...

    Wenn Du einen Index mitführst, benötigst Du natürlich nur die Sätze, die in den Indexbereich passen. Ähnliches gilt für sortierte Dateien.

    Du kannst allerdings die Dateien auch als verkettete Listen oder Bäume aufbauen. Wie tief willst Du einsteigen?

    Harzliche Grüße aus http://www.annerschbarrich.de

    Tom

    --
    Fortschritt entsteht nur durch die Auseinandersetzung der Kreativen
    Nur selber lernen macht schlau
    1. Hi Tom!

      Ich dachte mir schon, dass das etwas längeres nachziehen würde... ;-)

      Wenn Datensätze als geschlossene Gruppen angereiht (egal ob mit wahlfreiem Zugriff oder mit sequentiellem) abgespeichert werden, hast Du keine andere Wahl, als die ganze Datei "durchzuziehen". Man kann das Satz für Satz oder Bereich für Bereich machen...

      Könntest du mir die beiden Begriffe "wahlfreier und sequentieller" Zugriff näher erläutern?

      Wenn Du einen Index mitführst, benötigst Du natürlich nur die Sätze, die in den Indexbereich passen. Ähnliches gilt für sortierte Dateien.

      Was würde dann in einem solchen Index für meinen Fall stehen?

      Du kannst allerdings die Dateien auch als verkettete Listen oder Bäume aufbauen. Wie tief willst Du einsteigen?

      Nunja, ich habs ja jetzt soweit das es funktioniert, aber wie gesagt erscheint es mir nicht die beste Lösung zu sein. Welche der von dir genannten Möglichkeiten erscheint dir für mein Problem als die sinnvollste? Am Ende wird die Datei ca. 150 Datensätze mit je 20 Felder enthalten. Also im Vergleich zu meinem Beispiel eine doch erhebliche Steigerung.

      Grüße,
      Fabian St.

      --
      Meine Website: http://fabis-site.net
      --> XHTML, CSS, PHP-Formmailer, Linux
      ---------------------
      fabi@erde ~# whatis spam
      spam: nothing appropriate
      ---------------------
      Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
      1. Hello,

        Könntest du mir die beiden Begriffe "wahlfreier und sequentieller" Zugriff näher erläutern?

        grundsätzlich sind Dateien auf PC-Plattformen bei den üblichen Filesystemen alle sequentiell aufgebaut. Es wird (aus Sicht der Applikation) ein Byte nach dem anderen geschrieben. Wie das auf Festplattenebene tatsächlich passiert (Cluster) bleibt meistens Geheimnis des Betriebssystems.

        Wenn Du jetzt Datensätze nach dem CSV-Prinzip abspeicherst

        "001","Thomas","Schmieder","""nachricht"" ist ein schönes wort"|"002","Fabian","Der Große","Wer hilft bei der Problemlösung?"|"003","","Hat keinen Vornamen","123456789|abcdefghi|"|

        Das Pipe-Symbol habe ich hier immer für das Satzende-Zeichen (Zeilenumbruch) benutzt.

        "001","Thomas","Schmieder","""nachricht"" ist ein schönes wort"|
        "002","Fabian","Der Große","Wer hilft bei der Problemlösung?"|
        "003","","Hat keinen Vornamen","123456789|abcdefghi|"|

        Wenn man das nun anders schreibt, sieht man, dass die Satzlängen und die Feldlängen unterschiedlich sind, man also nicht einfach die Position eines Datensatzes oder gar eines einzelnen Feldes in der Menge (Bindfaden) bestimmen kann.

        Wenn Man jetzt daraus eine Satzstruktur mit fester Satzlänge und dann auch noch mit festem Satzaufbau macht (fese Feldlänge), dann verschwendet man zwar Speicherplatz auf der Festplatte, kann aber die Einsprungspunkte berechnen.

        001ThomasSchmieder          "nachricht"" ist ein schönes wort.
        002FabianDer Große          Wer hilft bei der Problemlösung? .
        003      Hat keinen Vornamen123456789|abcdefghi|             .

        Die Punkte stehen hier nur als optische Begrenzung für die Sätze.
        Man sieht, dass man weder Zeilenumbrüche benötigt, um die Sätze voneinander zu trennen (die Punkte werden also nicht mit gespeichert) noch Trennzeichen zwischen den Feldern, denn man weiß ja (das merkt man sich irgendwo) wie lang jedes Feld ist. Man kann also wahlfrei (Random Access) auf jedes beliebige Feld zugreifen (berechnen).

        Harzliche Grüße aus http://www.annerschbarrich.de

        Tom

        --
        Fortschritt entsteht nur durch die Auseinandersetzung der Kreativen
        Nur selber lernen macht schlau
        1. Hi Tom!

          Danke für die Erklärungen, Tom! Den Begriff des wahlfreien Zugriffs und seiner Vorteile habe ich nun verstanden.
          Demnach bietet sich Random Access immer dann an, wenn man in besonderen Maßen nur auf Teile des Inhalts einer Datei zugreifen möchte und nicht auf den ganzen Inhalt. Das hat dann jedoch wieder den Nachteil, dass die Länge eines Feldes fest vorgegeben ist, man aber dadurch z.B. mit fseek() (?) auf genau die Position kommt, wo das steht, was man braucht. Sehe ich das soweit richtig?

          Grüße,
          Fabian St.

          --
          Meine Website: http://fabis-site.net
          --> XHTML, CSS, PHP-Formmailer, Linux
          ---------------------
          fabi@erde ~# whatis spam
          spam: nothing appropriate
          ---------------------
          Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
          1. Hello,

            Demnach bietet sich Random Access immer dann an, wenn man in besonderen Maßen nur auf Teile des Inhalts einer Datei zugreifen möchte und nicht auf den ganzen Inhalt. Das hat dann jedoch wieder den Nachteil, dass die Länge eines Feldes fest vorgegeben ist, man aber dadurch z.B. mit fseek() (?) auf genau die Position kommt, wo das steht, was man braucht. Sehe ich das soweit richtig?

            Dafür ist fseek() da.
            Die PC-Dateisysteme sind i.d.R. so gebaut, dass man auch ab dieser Stelle gezielt schreiben kann.
            Im Hintergrund funktioniert das noch ganz anders: da muss jedes Mal der gesamte Cluster eingelesen und wieder weggeschrieben werden, auch wenn nur ein einziges Byte darin betroffen ist. Das hängt mit den Synchronisationsmarken und Toleranzen beim Lesen und Schreiben auf der HDD zusammen. Ein Byte landet (microsopisch betrachtet) selten wieder genau am selben Platz.

            Das Dateisystem ist davon aber unabhängig, da es mit Buffern arbeitet.

            Man kann nun natürlich beliebig komplizierte Systeme aufbauen, die den Zugriff auf einen bestimmten Datenwert berechenbar machen.

            Du könntest als Übung mal versuchen, eine Datei nach einem bestimmten Feldinhalt zu sortieren, ohne sie dabei vollständig einzulesen. Stichworte: Bubblesort, Quicksort, Sortieralgorithmen, ...

            Harzliche Grüße aus http://www.annerschbarrich.de

            Tom

            --
            Fortschritt entsteht nur durch die Auseinandersetzung der Kreativen
            Nur selber lernen macht schlau
  2. Hello,

    id | vname | nname | nachricht

    Noch ein Tipp:
    Wenn Du eine echte CSV-Datei benutzt, brauchst Du dich um Sonderzeichen nicht mehr sorgen...

    "id","vname","nname","""nachricht"" ist ein schönes Wort"

    Harzliche Grüße aus http://www.annerschbarrich.de

    Tom

    --
    Fortschritt entsteht nur durch die Auseinandersetzung der Kreativen
    Nur selber lernen macht schlau
    1. Hi!

      id | vname | nname | nachricht

      Noch ein Tipp:
      Wenn Du eine echte CSV-Datei benutzt, brauchst Du dich um Sonderzeichen nicht mehr sorgen...

      "id","vname","nname","""nachricht"" ist ein schönes Wort"

      Ich soll also Kommas anstelle der Pipe verwenden und die einzelnen Datensätze in Anführungszeichen setzen?
      Hast du sonst noch Anmerkungen zum Code?

      Grüße,
      Fabian St.

      --
      Meine Website: http://fabis-site.net
      --> XHTML, CSS, PHP-Formmailer, Linux
      ---------------------
      fabi@erde ~# whatis spam
      spam: nothing appropriate
      ---------------------
      Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
      1. Hello,

        id | vname | nname | nachricht

        Noch ein Tipp:
        Wenn Du eine echte CSV-Datei benutzt, brauchst Du dich um Sonderzeichen nicht mehr sorgen...

        "id","vname","nname","""nachricht"" ist ein schönes Wort"

        Ich soll also Kommas anstelle der Pipe verwenden und die einzelnen Datensätze in Anführungszeichen setzen?

        Das ist nur ein Beispiel für den Aufbeu einer CSV-Datei.
        Bei vernünftigen Programmen kann man die Begrenzer und die Trenner einstellen. M$-Programme, die z.B. für einen Import in Frage kommen (Excel) versuchen das aber inzwischen alles automatisch und akzeptieren daher eigentlich nur Komma und Doppelhäkchen ohne Probleme.

        Wichtig ist, dass die Begrenzer (") gedoppelt werden (maskiert), wenn sie innerhalb des begrenzten Feldes vorkommen. Wenn Du die CSV-Datensätze so aufbaust, können die Feldwerte z.B. auch den Zeilenumbruch oder NULL enthalten, ohne dass es Probleme gibt.

        Besser wäre der Aufbau als Random Access File, so wie ich es in der "Adressverwaltung" gemacht habe.

        Harzliche Grüße aus http://www.annerschbarrich.de

        Tom

        --
        Fortschritt entsteht nur durch die Auseinandersetzung der Kreativen
        Nur selber lernen macht schlau
  3. Hallo Fabian,

    Dein Ansatz ist ok. Im Vergleich von explode(), split() und preg_split() ist explode() sie schnellste Methode und sollte deshalb bevorzugt zum Einsatz kommen. Strings innerhalb von doppelten Anfuehrungszeichen werden evaluiert, Strings in einfachen Anfuehrungszeichen einfach durchgereicht, deshalb sind einfache Anfuehrungszeichen, wenn immer moeglich, vorzuziehen. Um volle Kompatibilitaet mit Windows sicherzustellen, empfiehlt es sich, im 2. fopen()-Argument entweder b (=binary) oder t (=text) mitzugeben, siehe Manual bei fopen(). Ueberhaupt frage ich mich, warum du die Datei nicht mit file() einliest, das gleich ein Array zurueckgibt. Dabei ist aber zu beachten, dass du, wenn du die Werte des Arrays (=Zeilen der Datei) bearbeitest, trim() anwenden musst, da file() immer ein paar \n oder \0 am Zeilenende mitschleppt. Mit file() waerst du gleich hier:
    foreach($lines as $line)... und muesstest evtl. mit einem if checkecken, ob der Eintrag der Zeile deinen Kriterien entspricht.
    Ich halte es fuer eleganter, anstatt jede Zeile mit einem echo auszugeben, hier -> echo "<table border="1">\n"; einen String mit dem gleichen Inhalt anzufangen, und in der foreach Schleife dann die Werte so anzuhaengen:
    $table = "<table border="1">\n";
    foreach($lines as $line)
    {
      $_line_parts = explode("|", $line);
      $table .= "\t<tr><td>Vorname:</td><td><strong>".$_line_parts[1]."</strong></td></tr>\n"
             . "\t<tr><td>Nachname:</td><td><strong>".$_line_parts[2]."</strong></td></tr>\n"
             . "\t<tr><td>Nachricht:</td><td><strong>".$_line_parts[3]."</strong></td></tr>\n"
             . "\t<tr><td colspan="2">&nbsp;</td></tr>\n";
    }
    Das gilt auch analog fuer die anderen echos, Der Vorteil ist, dass du am Schluss die fertige Tabelle in einer Variablen hast, die du sauber im HTML-Code unterbringen kannst (Trennung von Code und Ausgabe, aehnlich wie die Trennung von HTML und CSS). Konkatinieren (mit . aneinanderhaengen) wie im Beispiel, geht auch mit print(), echo etc., sodass man nicht immer sie Funktion neu aurufen muss.
    Persoenlich mag ich auch \t nicht, weil jeder Editor eine andere Auffassung von Tabluatoren hat. Mit Leerzeichen (ich nehme 2) ist gewaehrleistet, dass der Code ueberall gleich aussieht (hoffentlich zumindest).
    Aber Dein Script wird durch diese Optimierungen natuerlich nicht rasend schnell, da es ohnehin sehr klein ist und kaum was zu tun hat.

    Gruß,

    Dieter

    1. Hi Dieter!

      Um volle Kompatibilitaet mit Windows sicherzustellen, empfiehlt es sich, im 2. fopen()-Argument entweder b (=binary) oder t (=text) mitzugeben, siehe Manual bei fopen().

      Ich habe mir natürlich den Manual-Eintrag zu fopen() durchgelesen, aber da ich hier ausschließlich mit Linux arbeite und das Skript dann auch später auf einem Linux-Server laufen wird, spielt die Kompatibilität zu Windows erst einmal eine untergeordnete Rolle.

      Ueberhaupt frage ich mich, warum du die Datei nicht mit file() einliest, das gleich ein Array zurueckgibt. Dabei ist aber zu beachten, dass du, wenn du die Werte des Arrays (=Zeilen der Datei) bearbeitest, trim() anwenden musst, da file() immer ein paar \n oder \0 am Zeilenende mitschleppt. [...]

      Stimmt, das wäre eine Überlegung wert. Da es doch einige Datei-Funktionen gibt, die manchmal nur ein Alias für eine andere sind (fputs <--> fwrite), fällt am Anfang der Überblick nicht so leicht ;-)

      foreach($lines as $line)... und muesstest evtl. mit einem if checkecken, ob der Eintrag der Zeile deinen Kriterien entspricht.
      Ich halte es fuer eleganter, anstatt jede Zeile mit einem echo auszugeben, hier -> echo "<table border="1">\n"; einen String mit dem gleichen Inhalt anzufangen, und in der foreach Schleife dann die Werte so anzuhaengen [...]

      Ja klar, später wird das ganze auch noch schön in Funktionen verpackt, die dann z.B. die ganze Tabelle zurückgeben.

      Aber Dein Script wird durch diese Optimierungen natuerlich nicht rasend schnell, da es ohnehin sehr klein ist und kaum was zu tun hat.

      Muss es auch nicht sein. Ich war mir eben nicht sicher, ob ich vielleicht etwas übersehen habe, was das ganze einfacher gestalten würde. Wie ich bereits Tom in einem anderen Posting schrieb, wird die Datei später wahrscheinlich noch stark anwachsen und mir ist eben nicht ganz wohl bei der Sache, die _ganze_ Datei einlesen zu müssen, um dann die Sachen zu bekommen, die ich eigentlich will.

      Grüße,
      Fabian St.

      --
      Meine Website: http://fabis-site.net
      --> XHTML, CSS, PHP-Formmailer, Linux
      ---------------------
      fabi@erde ~# whatis spam
      spam: nothing appropriate
      ---------------------
      Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
      1. Hallo Fabian,

        fällt am Anfang der Überblick nicht so leicht ;-)

        Das Gute an PHP ist die Fuelle der Funktionen, das Schlchte an PHP ist die Fuelle der Funktionen. Ich stolpere auch staendig ueber irgendeine Funktion, die mir voellig neu ist.

        die _ganze_ Datei einlesen zu müssen, um dann die Sachen zu bekommen, die ich eigentlich will.

        Da fuehrt kein Weg dran vorbei, fuerchte ich.
        Du kannst ja mal mit PHP ein CSV-File basteln, dass ein paar 1000 Eintraege hat, eine Telefonbuch-CD eignet sich da wahrscheinlich ganz gut als Quelle. Dann faengst du vorher und hinterher mit microtime() die Laufdauer ab, um zu sehen, wie schnell dein Script ist. Aus dem gleichen CSV kannst du eine Datenbank machen, damit du einen Vergleich hast.

        Gruß,

        Dieter

  4. Hi!

    Kleine Rückmeldung:

    Hier einige Werte, wie lange das Lesen bzw. Schreiben gedauert hat:

    Server    | Datensatz schreiben + alle anzeigen | alle Datensätze mit gleichem Namen suchen
    -------------------------------------------------------------------------------------------------
    Gentoo Linux  |  0.3453s                             |  0.0026s
    Apache 2.0.53 |
    PHP 5.0.3     |
    -------------------------------------------------------------------------------------------------
    1&1           | 3.3398s                              |  0.37152s
    Apache 1.3.29 |
    PHP 4.3.10    |

    Die Zeiten sind jeweils gemessen zwischen fopen() und fclose(). Die Größe der Datei beträgt 445,643kb.

    Grüße,
    Fabian St.

    --
    Meine Website: http://fabis-site.net
    --> XHTML, CSS, PHP-Formmailer, Linux
    ---------------------
    fabi@erde ~# whatis spam
    spam: nothing appropriate
    ---------------------
    Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
    1. Hallo Fabian,

      Die 3.3 sec bei 1&1 kommen mit sehr viel vor, das solltest du vielleicht nochmal checken. Hast du einen Vergleich mit file()?

      Gruß,

      Dieter

      1. Hi Dieter!

        Die 3.3 sec bei 1&1 kommen mit sehr viel vor, das solltest du vielleicht nochmal checken. Hast du einen Vergleich mit file()?

        Du kannst es ja selber noch einmal probieren: http://fabis-site.net/tmp/split.html
        Einfach irgendwelche Werte eingeben und dann abschicken. Um das Ergebnis zu vorhin nicht zu verfälschen, sollten möglichst kurze Strings eingegeben werden.

        Mit file() werde ich es gleich einmal probieren...

        Grüße,
        Fabian St.

        --
        Meine Website: http://fabis-site.net
        --> XHTML, CSS, PHP-Formmailer, Linux
        ---------------------
        fabi@erde ~# whatis spam
        spam: nothing appropriate
        ---------------------
        Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
  5. Hi!

    Update (Einlesen der Datei mit file()):

    Server    | Datensatz schreiben + alle anzeigen | alle Datensätze mit gleichem Namen suchen
    -------------------------------------------------------------------------------------------------
    Gentoo Linux  |  0.2473s                             |  0.00278s
    Apache 2.0.53 |
    PHP 5.0.3     |
    -------------------------------------------------------------------------------------------------
    1&1           | 3.3353s                              |  0.37113s
    Apache 1.3.29 |
    PHP 4.3.10    |

    ==> Kein Unterschied, bei sonst gleichen Bedingungen

    Grüße,
    Fabian St.

    --
    Meine Website: http://fabis-site.net
    --> XHTML, CSS, PHP-Formmailer, Linux
    ---------------------
    fabi@erde ~# whatis spam
    spam: nothing appropriate
    ---------------------
    Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
    1. Hallo Fabian,

      vermutlich hast Du auf dem Testrechner PHP als Modul eingebunden. Soweit mir bekannt ist, setzt 1&1 PHP als CGI ein, weiterhin ist auch zu vermuten, daß Maßnahmen von 1&1 getroffen worden sind, die die Rechenleistung pro Account drosseln, um schlechten Scripten keine Chance zu geben, die Maschine voll auszulasten.

      Gruß aus Berlin!
      eddi

      1. Hi eddi!

        vermutlich hast Du auf dem Testrechner PHP als Modul eingebunden. Soweit mir bekannt ist, setzt 1&1 PHP als CGI ein, weiterhin ist auch zu vermuten, daß Maßnahmen von 1&1 getroffen worden sind, die die Rechenleistung pro Account drosseln, um schlechten Scripten keine Chance zu geben, die Maschine voll auszulasten.

        Ich habe auf meinem Rechner beiden Arten, d.h. sowohl das Modul als auch die CGI-Version installiert und kann so bei Bedarf "switchen" ;-)
        Wenn ich das Skript als CGI-Version laufen lassen, dauert das ganze ca. 0.1 bis 0.3s länger, sodass ich mir nach wie vor die 3.3s nicht erklären kann.
        Bzgl. der Drosselung seitens 1&1 kann ich nichts sagen, vielleicht wende ich mich mal an den Support.

        Auf alle Fälle versuche ich jetzt mal, so eine Random-Access-File aufzubauen...

        Grüße,
        Fabian St.

        --
        Meine Website: http://fabis-site.net
        --> XHTML, CSS, PHP-Formmailer, Linux
        ---------------------
        fabi@erde ~# whatis spam
        spam: nothing appropriate
        ---------------------
        Selfcode: ie:% fl:|  br:^ va:) ls:& fo:) rl:( n4:° ss:| de:> js:| ch:| mo:) zu:)
  6. echo $begrueszung;

    irgendwelche Verbesserungsvorschläge bzgl. des obigen Stück Codes habt.

    um CSV-Dateien zu lesen bietet PHP eine eigene Funktion an: fgetcsv()

    Deine split.php auf fgetcsv() umgeschrieben brachte zwar eine messbar höhere Laufzeit ein, es könnte aber sein, dass fgetcsv performanter ist, wenn man von Enclosure Gebrauch macht und auch der Delimiter Bestandteil der Values sein darf. Mit ein paar einfachen explode()s kommt man dann nicht mehr hin...

    Außerdem gibt es eine Warning und ein paar Notices, wenn die split.dat leer ist.

    echo "$verabschiedung $name";