Enrico: PHP: Rekursives Einlesen von Ordnern und Dateien liefert doppelte Werte

Hallo,

sorry, euch wieder bemühen zu müssen, aber ich komme bei einem blöden Problem einfach nicht weiter.

Ich habe folgende Funktion, um rekursiv ein Verzeichnis samt Unterordnern und enthaltener Dateien einzulesen:

function elementeEinlesen ($pfad, &$resultat = [])
{
   $elemente = scandir ($pfad);

   foreach ($elemente as $element)
   {
      if (!is_dir ($pfad . DIRECTORY_SEPARATOR . $element))
      {
         $resultat[] = $element;
      }
      else
      {
         if ($element != "." && $element != "..")
         {
            elementeEinlesen ($pfad . DIRECTORY_SEPARATOR . $element, $resultat[$element]);

            $resultat[] = $element;
         }
      }
   }

   return $resultat;
}

Hierbei erhalte ich in meiner Testumgebung folgende Ausgabe:

    Array (4)
    (
    |    ['fotos'] => Array (2)
    |    (
    |    |    ['live'] => Array (2)
    |    |    (
    |    |    |    ['17_3_28_Schrobenhausen'] => Array (8)
    |    |    |    (
    |    |    |    |    ['0'] = String(5) "1.jpg"
    |    |    |    |    ['1'] = String(5) "2.jpg"
    |    |    |    |    ['2'] = String(5) "3.jpg"
    |    |    |    |    ['3'] = String(5) "4.jpg"
    |    |    |    |    ['4'] = String(5) "5.jpg"
    |    |    |    |    ['5'] = String(5) "6.jpg"
    |    |    |    |    ['6'] = String(5) "7.jpg"
    |    |    |    |    ['7'] = String(5) "8.jpg"
    |    |    |    )
==> |    |    |    ['0'] = String(22) "17_3_28_Schrobenhausen"
    |    |    )
==> |    |    ['0'] = String(5) "live"
    |    )
==> |    ['0'] = String(6) "fotos"
    |    ['presse'] => Array (3)
    |    (
    |    |    ['0'] = String(13) "bandfotos.zip"
    |    |    ['1'] = String(13) "bandlogos.zip"
    |    |    ['2'] = String(24) "informationsmaterial.pdf"
    |    )
==> |    ['1'] = String(7) "presse"
    )

Die mit voranstehenden Pfeilen markierten Einträge sind "fälschlicherweise" doppelt angelegt.

Was habe ich falsch gemacht?

Vielen, vielen lieben Dank für eure Hilfe und Gruß
Enrico

akzeptierte Antworten

  1. Hallo Enrico,

    Was habe ich falsch gemacht?

    soweit ich sehe, nichts. Scandir liefert, wie es sein sollte, außer Dateien auch die Verzeichnisse auf. Die Zusatzinfo brauchst du ja nicht nutzen, oder eben von vornerein ausfiltern. Kann mich aber auch irren, mache so was meistens mit GLOB.

    Gruss
    Henry

  2. Tach!

    Die mit voranstehenden Pfeilen markierten Einträge sind "fälschlicherweise" doppelt angelegt.

    Was habe ich falsch gemacht?

    Offensichtlich ist deine Programmlogik nicht wie sie sein sollte. Debuggen heißt die unbeliebte Methode, um solchen Fehlern auf die Spur zu kommen. An den Stellen, an denen du dein Ergebnis-Array erweiterst, könntest du eine Ausgabe einfügen, die dir zeigt, wann dein Code an den jeweiligen Stellen vorbeikommt. Am besten auch noch am Begin und Ende der Funktion. Zu den Ausgaben könntest du auch noch die aktuellen Werte einfügen, also den Dateinamen und im Falle der Funktionsaufrufkontrolle den Pfad.

    dedlfix.

    1. Tach!

      Man kann sich das Selberschreiben der rekursiven Funktion auch sparen und zum RecursiveDirectoryIterator (nebst RecursiveIteratorIterator) greifen. (Geht auch mit foreach, muss kein while wie im dortigen Beispiel sein.)

      dedlfix.

      1. Hallo dedlfix,

        ob man den rekursiven Iterator nehmen kann, hängt wohl auch davon ab, ob man nachher ein rekursives Array haben will oder nicht :)

        Andernfalls bietet sich der FileSystemIterator an, der ganz nebenbei auch das Handling von ".", ".." und SymLinks übernimmt.

        Rolf

        --
        Dosen sind silbern
  3. Hallo Enrico,

    im else-Zweig fügst du den Verzeichnisnamen zum zweiten Mal ein. So müsste es gehen:

    function elementeEinlesen ($pfad, &$resultat = [])
    {
       $elemente = scandir ($pfad);
    
       foreach ($elemente as $element)
       {
          if (!is_dir ($pfad . DIRECTORY_SEPARATOR . $element))
          {
                $resultat[] = $element;
          }
          elseif ($element != "." && $element != "..")
          {
                elementeEinlesen ($pfad . DIRECTORY_SEPARATOR . $element, $resultat[$element]);
          }
       }
    
       return $resultat;
    }
    

    Nachtrag: Bei der Fehlersuche bin ich so vorgegangen, wie dedlfix es bereits schilderte.

    Gruß
    Julius

    1. Hallo Ingrid,

      vielleicht noch schöner, weil es ohne die Übergabe per Referenz auskommt und das elseif bei jedem Durchlauf einspart (Benutzung von array_diff):

      function elementeEinlesen ($pfad)
      {
         $elemente = array_diff(scandir($pfad), ['.', '..']);
      
         foreach ($elemente as $element)
         {
            if (!is_dir ($pfad . DIRECTORY_SEPARATOR . $element))
            {
                  $resultat[] = $element;
            }
            else
            {
                  $resultat[$element] = elementeEinlesen ($pfad . DIRECTORY_SEPARATOR . $element);
            }
         }
      
         return $resultat;
      }
      

      Gruß
      Julius

      1. Hallo Enrico, hallo Juligrid,

        das ist so noch keine gute Lösung. Das Ergebnis des Verzeichnis-Scans ist eine Array-Hierarchie, in der Dateien einen numerischen Key tragen und Verzeichnisse ihren Namen. Es bricht aber die Hölle los, wenn ein Verzeichnis einen Namen wie "007" trägt. Wenn zuerst 18 Dateien gefunden werden und dann dieses Verzeichnis kommt, überschreibt es den Eintrag für die 8. Datei. Immerhin hast Du Glück, dass PHP an dieser Stelle keine Erkennung auf Oktal oder Hexadezimal durchführt, sonst würde ein Verzeichnisname wie "009" einen „Invalid numeric literal“ Fehler erzeugen.

        Hier werden verschiedene Wertedomänen als Quelle für Array-Indizes vermischt, das darf man nicht tun. Die Kollision ist vorprogrammiert.

        Wenn Du den Namen des Filesystem-Objekts als Array-Key verwenden willst, dann sei damit konsequent. Für Verzeichnisse verwendest Du als Wert das Array mit deren Inhalt, und für einfache Dateien irgendwas, was kein Array ist. Zum Beispiel einen String "FILE" oder einfach TRUE. Wenn Du den gelesenen Verzeichnis-Baum nachher traversierst, kannst Du mit is_array feststellen, ob der jeweilige Eintrag eine Datei ist oder ein Unterordner.

        Wenn Du die laufende Nummer als Array-Key verwenden wilst, musst Du pro gefundenen Filesystem-Objekt ein Informationsobjekt erstellen, und dieses Informationsobjekt ins $resultat-Array schreiben. Das Informationsobjekt kann durch eine PHP-Klasse gebildet werden (in dem Fall eine Hierarchie: FilesystemObject mit den Subklassen DirectoryEntry und FileEntry), oder du machst es „klassisch“ mit einem simplen Array. Erster Array-Eintrag ist der Name der Datei oder des Ordners, und bei Ordnern gibt's einen zweiten Eintrag mit dem $resultat-Array dieses Ordners.

        Die erste Lösung ist eigentlich die PHP-typischere. Da PHP Arrays keine Hashtables sind, bleibt darin sogar die Reihenfolge erhalten, in der Du die Dateien gefunden hast (falls das wichtig ist).

        Wie auch immer. Entweder numerische Indizes ODER Dateinamen. Nicht vermischt.

        Rolf

        --
        Dosen sind silbern
        1. Hallo Rolf,

          perfekt!

          Dein ausführlicher Thread hat mich zum gewünschten Ziel geführt, danke Dir!

          Gruß
          Enrico

      2. Hallo Julius,

        $elemente = array_diff(scandir($pfad), ['.', '..']);

        hast Du gemessen, ob das effizienter ist? Es ist natürlich meistens besser, optimierte Library-Funktionen zu verwenden statt PHP Code, aber array_diff ist eine aufwändige Operation.

        Was bremsen kann, ist, dass array_diff für jeden Eintrag einen Vergleich mit zwei Array-Werten durchführen muss, nicht nur für Directories. Das else if testet nur, wenn tatsächlich ein Directory vorliegt.

        Rolf

        --
        Dosen sind silbern
        1. Hallo Rolf,

          $elemente = array_diff(scandir($pfad), ['.', '..']);

          hast Du gemessen, ob das effizienter ist?

          Erwischt, das habe ich nicht getan.

          Es ist natürlich meistens besser, optimierte Library-Funktionen zu verwenden statt PHP Code, aber array_diff ist eine aufwändige Operation.

          Vermutlich O(n²) vs. O(n), Array-Operation vs. Stringvergleich – ich glaube, da ist ein Benchmark sinnlos...

          Gruß
          Julius

          P.S.: Ist mit deinem Nickname alles in Ordnung? 😉

          1. Hallo Julius,

            ich glaube nicht, dass array_diff prinzipiell O(n²) hat, weil ja nicht ein Array mit sich selbst gematcht wird, sondern zwei Arrays miteinander, wovon eins fix ist. Die Laufzeitkomplexität ist also O(2n), und das ist O(n). Die relevante Frage ist nach dem Anteil an Verzeichnissen in den gefundenen Filesystemobjekten - der könnte sinken wenn man größere Bäume traversiert, und an der Effizienz von array_diff an sich.

            Mein Nickname ist prima in Ordnung. Der sollte nur die Behauptung von CK und GB testen, dass Unicodezeichen außerhalb von Basic Latin und Latin-1 Supplement eine eigene CSS Klasse bekommen. Dumm nur, dass das kleine ß in Latin-1 liegt, werde das mal ändern.

            Edit: Yup - class .suspicious greift.

            Rolf

            --
            Dosen sind silbern
            1. @@Rolf ẞ

              Ich wollte dir schon das große ẞ nahelegen – da haste es schon selbst für dich entdeckt.

              LLAP 🖖

              --
              “When UX doesn’t consider all users, shouldn’t it be known as ‘Some User Experience’ or... SUX? #a11y” —Billy Gregory
            2. Hallo Rolf,

              ich glaube nicht, dass array_diff prinzipiell O(n²) hat, weil ja nicht ein Array mit sich selbst gematcht wird, sondern zwei Arrays miteinander, wovon eins fix ist. Die Laufzeitkomplexität ist also O(2n), und das ist O(n).

              Aaah. Danke fürs Aufklären meines Irrtums! Als ich – leider erfolglos – versucht habe, den Quellcode der Originalimplementierung von array_diff in PHP ausfindig zu machen (bin wohl zu doof 😟), bin ich auf diesen Beispielcode gestoßen, wo mir das auch klar wurde.

              Die relevante Frage ist nach dem Anteil an Verzeichnissen in den gefundenen Filesystemobjekten - der könnte sinken wenn man größere Bäume traversiert, und an der Effizienz von array_diff an sich.

              Du meinst, dass sich array_diff bei einer gewissen Menge an Dateien pro Ordner ggü. der Lösung mit den if’s in foreach im Vorteil ist?


              Ich habe – weil es mich interessiert – einen kleinen Testaufbau (s.u.) gebastelt und ein wenig gebenchmarkt. Wenn ich die (Beispiel-)Werte ['.', '..', '.htaccess'] ausschließe, ist bei 1.000.000 (Zahlen bei 100.000 und 10.000 Einträgen ähnlich) Einträgen im Array array_diff tatsächlich im Vorteil. Beispiel-Werte (weniger ist besser, foreach mit if’s ist 100):

              |foreach|100| |array_diff|61| |foreach in_array|108|

              Geht man vom Anwendungsfall des TO aus (nur ['.', '..'] ausschließen, eher kleine Array-Größen), nehmen sich die Funktionen nicht mehr viel. Beispiel-Werte:

              |foreach|100| |array_diff|90| |array_slice|40| |foreach in_array|140|

              Nach diesem schnellen Test scheinen array_diff bzw. array_slice (falls es nur um die ersten beiden Werte im Array – .. und . – geht) eine gute Alternative für das if im foreach zu sein. Ich habe bei den (mehrfach ausgefühten) Tests ab und an einen Ausreißer gehabt, ich habe dann grob passende Ausgaben genommen (hier könnte man natürlich mehrere Messreihen ausführen und dann Mittelwert oder Median benutzen). Außerdem bin ich mir nicht sicher, ob PHP da nicht doch etwas wegoptimiert.

              Der Code (quick and dirty):

              <?php
              // Hilfsfunktionen:
              function generateRandomString($length = 10) {
                  $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_,;.:#\'\\+~*()/&$"!?';
                  $charactersLength = strlen($characters);
                  $randomString = '';
                  for ($i = 0; $i < $length; $i++) {
                      $randomString .= $characters[rand(0, $charactersLength - 1)];
                  }
                  return $randomString;
              }
              
              function fillArrayRandomStr($size=20) {
                $result = [];
                for($i=0; $i<$size; $i++) {
                  $result[] = generateRandomString(rand(1, 25));
                }
                return $result;
              }
              
              // Ausgabe von scandir() simulieren:
              $a = fillArrayRandomStr(1000);
              $a = array_merge(['.', '..'], $a);
              
              $ts = microtime(true);
              foreach($a as $val) {
                if($val != '.' && $val != '..') {
                  //continue;
                }
              }
              $_foreach = microtime(true) - $ts;
              echo "foreach\t\t=".round($_foreach/$_foreach*100)."\n";
              
              $ts = microtime(true);
              $b = array_diff($a, ['.', '..']);
              foreach($b as $val) {
              }
              echo "array_diff\t=".round((microtime(true) - $ts)/$_foreach*100)."\n";
              
              $ts = microtime(true);
              $b = array_slice($a, 2);
              foreach($b as $val) {
              }
              echo "array_slice\t=".round((microtime(true) - $ts)/$_foreach*100)."\n";
              
              $ts = microtime(true);
              foreach($a as $val) {
                if(!in_array($val, ['.', '..'])) {
                  //continue;
                }
              }
              echo "foreach in_array=".round((microtime(true) - $ts)/$_foreach*100)."\n";
              ?>
              

              Verwendete PHP-Version:

              PHP 7.0.18-0ubuntu0.16.04.1 (cli) ( NTS )
              Copyright (c) 1997-2017 The PHP Group
              Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
                  with Zend OPcache v7.0.18-0ubuntu0.16.04.1, Copyright (c) 1999-2017, by Zend Technologies
              

              Mein Nickname ist prima in Ordnung. Der sollte nur die Behauptung von CK und GB testen, dass Unicodezeichen außerhalb von Basic Latin und Latin-1 Supplement eine eigene CSS Klasse bekommen. Dumm nur, dass das kleine ß in Latin-1 liegt, werde das mal ändern.

              Das header-Element bekommt die Klasse suspicious also zugewiesen. Ich musste dreimal die Entwicklertools öffnen, bevor ich das gesehen habe 😂.

              Edit: Yup - class .suspicious greift.

              Rein Interessenshalber: Warum behältst du dann das ẞ bei? Um die anderen Nutzer zu verwirren (wäre ein valider Grund 😉)? Zumindest bei Sophie hat das ja scheinbar geklappt...

              Gruß
              Julius

  4. Hello,

    die Funktion kann sich mMn noch aufhängen, da Du symbolische Links nicht abgefangen hast.

    Liebe Grüße
    Tom S.

    --
    Es gibt nichts Gutes, außer man tut es
    Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
    1. Hallo Tom,

      die Funktion kann sich mMn noch aufhängen, da Du symbolische Links nicht abgefangen hast.

      Stimmt. is_link sollte helfen.

      Man müsste sich nur überlegen, ob man solche Links ignoriert oder ob man prüft, ob man den Ordner, auf den sie verweisen, schon eingelesen hat.

      Gruß
      Julius

      1. Hello,

        die Funktion kann sich mMn noch aufhängen, da Du symbolische Links nicht abgefangen hast.

        Stimmt. is_link sollte helfen.

        Man müsste sich nur überlegen, ob man solche Links ignoriert oder ob man prüft, ob man den Ordner, auf den sie verweisen, schon eingelesen hat.

        Ich hätte gerne den Rest auch noch dazu geschrieben, aber das war mir dann vom Tablet doch zu murksig...

        Deshalb jetzt nachgeliefert:

        • is_link() benutzen oder umstapeln in Sammelarray mit vorheriger Kontrolle, ob die Eintragung schon funktioniert.
        • als erstes die Treffer immer auflösen mit realpath(), dann gibts auch Überraschungen mit merkwürdigen relativen Namen und mit "007"-Dateien (siehe nächster Punkt)
        • dann kann man die resultierenden Namen als Schlüssel für das assoziative Ergebnisarray benutzen und kann bei DIRs vorher mit isset() prüfen, ob man sie schon bearbeitet hat. Als Values kann man sich dann z. B. ['type'] = "D", "F", "DL" oder "FL" mit abspeichern und sich ggf. noch die Größe, Datum und andere Angaben holen.

        Das Array ist dann bezüglich der Pfade linearisiert, also nicht mehr verschachtelt.

        Liebe Grüße
        Tom S.

        --
        Es gibt nichts Gutes, außer man tut es
        Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.