PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


6 Methoden, ein Verzeichnis rekursiv zu scannen

with 6 comments

Wer von euch kennt solchen Code:

if ($handle = opendir('/path/to/files')) {
    while (false !== ($file = readdir($handle))) {
        echo "$file\n";
    }
    closedir($handle);
}

So wird es häufig gemacht, aber hier wird natürlich nur ein Verzeichnis durchsucht und keine Unterordner betrachtet. Und häufig schleichen sich dort auch Fehler ein, beispielsweise wenn man im obigen Beispiel nur

while ($file = readdir($handle)) {

schreibt. Es gibt aber noch eine Menge anderer Möglichkeiten, die ich hier einmal vorstellen und vergleichen möchte. Danach stelle ich noch einige Testergebnisse vor, um die verschiedenen Möglichkeiten zu vergleichen sowie Vor- und Nachteile aufzuzeigen.

Hier erstmal das Script mit den einzelnen Funktionen:

$method = 'dirRead';

echo "mem before:".memory_get_peak_usage(true)."\n";
echo "Starting $method()\n";
clearstatcache();
$startTime = microtime(true);
$allFiles = DirectoryParser::$method('C:/Temp');
echo "Zeit: ".(microtime(true) - $startTime)." sec\n";
echo "mem after:".memory_get_peak_usage(true)."\n";
echo count($allFiles)."\n\n";

class DirectoryParser
{
	public static function createTestFolders($dir) {
		for($i=0; $i<10000; $i++) {
			@mkdir($dir.'/'.$i);
			for ($u=0; $u<10; $u++) {
				touch($dir.'/'.$i.'/'.rand(10000, 999999).'.txt');
			}
		}
	}

	public static function dirRead($dir, &$fileinfo = array()) {
		if ($handle = dir($dir)) {
			while (false !== ($file = $handle->read())) {
				if (!is_dir($dir.'/'.$file)) {
					$fileinfo[] = array($file, filesize($dir.'/'.$file));
				} elseif (is_dir($dir.'/'.$file) && $file != '.' && $file != '..') {
					self::dirRead($dir.'/'.$file, $fileinfo);
				}
			}
			$handle->close();
		}
		return $fileinfo;
	}

	public static function find($dir) {
		if (substr(php_uname(), 0, 7) == "Windows") {
			foreach (explode("\n",` cd "$dir" && dir /S /B /A-D `) as $fullFilename) {
				if ($fullFilename != '') {
					$fileinfo[] = array($fullFilename, filesize($fullFilename));
				}
			}
		} else {
			foreach (explode("\n",` cd $dir && find -maxdepth 3 -type f ! -name ".*" -printf "%f\r%s\n" `) as $fileInfos) {
				if ($fileInfos != '') {
					$fileinfo[] = explode("\r", $fileInfos);
				}
			}
		}
		return $fileinfo;
	}

	public static function glob($dir, &$fileinfo = array()) {
		foreach (glob($dir.'/*') as $file) {
			if (is_dir($file)) {
				self::glob($file, $fileinfo);
			} else {
				$fileinfo[] = array(basename($file), filesize($file));
			}
		}
		return $fileinfo;
	}

	public static function scandir($outerDir){
		$dirs = array_diff(scandir($outerDir), array(".", ".."));
		$fileArray = array();
		foreach ($dirs as $d) {
			if (is_dir($outerDir."/".$d)) {
				$fileArray = array_merge($fileArray, self::scandir($outerDir."/".$d));
			} else {
				$fileArray[] = array($d, filesize($outerDir."/".$d));
			}
		}
		return $fileArray;
	}

	public static function opendir($dir, &$fileinfo = array()) {
		if ($handle = opendir($dir)) {
			while (false !== ($file = readdir($handle))) {
				if (!is_dir($dir.'/'.$file)) {
					$fileinfo[] = array($file, filesize($dir.'/'.$file));
				} elseif (is_dir($dir.'/'.$file) && $file != '.' && $file != '..') {
					self::opendir($dir.'/'.$file, $fileinfo);
				}
			}
			closedir($handle);
		}
		return $fileinfo;
	}

	public static function directoryIterator($dir) {
		$iterator = new RecursiveDirectoryIterator($dir);
		foreach(new RecursiveIteratorIterator($iterator, RecursiveIteratorIterator::CHILD_FIRST) as $file) {
			if (false == $file->isDir()) {
				$fileinfo[] = array($file->getFilename(), $file->getSize());
			}
		}
		return $fileinfo;
	}
}

Man wählt also in Zeile 1 eine Methode, stellt in Zeile 7 den Pfad korrekt ein, und lässt das Script laufen. Falls man ein Testverzeichnis erstellen möchte mit 100.000 Dateien, kann man die Methode createTestFolders nutzen.

Bei allen Methoden, die Verzeichnisse durchsuchen und Dateien auslesen (z.B. um die Größe festzustellen) muss man darauf achten, nicht zuviele „offene Filehandles“ zu haben, denn viele Betriebssysteme haben da festgelegte Grenzen, wieviele offene Dateien ein Prozess haben darf.

Unter Linux findet man das beispielsweise heraus mit

$ cat /proc/sys/fs/file-max
184665
$ sysctl fs.file-max
fs.file-max = 184665

Leider weiß ich nicht, wie man die maximale Anzahl an gleichzeitigen FileDescriptors („open files“) eines laufenden Prozesses herausfindet, deshalb habe ich in der unten stehenden Tabelle nur die Gesamtanzahl der Filesystem-Events unter Windows 7 rausgeschrieben (herausgefunden mit dem Process Monitor). Wieviele davon jeweils gleichzeitig geöffnet waren konnte ich nicht herausfinden. Wer weiß Bescheid?

Des weiteren habe ich die Laufzeit protokolliert und auch die Algorithmen einmal einzeln laufen lassen, um den maximalen Speicherverbrauch herauszufinden.

MethodeLaufzeit (s)MemoryPeak (MB)FilesystemEvents Win7Probleme
dir()/read()51331207556
find/dir4639805840
glob()5634900006findet keine Dateien, die mit . beginnen
scandir()312391057467
opendir()/readdir()52331206817
DirectoryIterator87332146331

Die Messungen habe ich mit meiner normalen SATA2 Festplatte gemacht, das untersuchte Verzeichnis hat 90.499 Dateien, die meisten davon 0 Byte groß und insgesamt über 10078 Unterordner verteilt.

Interessanterweise sind die Ergebnisse unter Linux deutlich anders gewesen.

  • Einmal habe ich einen Ubuntu 9.10 Server genommen, mit SSD, aber leider relativ schwacher CPU. PHP 5.2.10
MethodeLaufzeit (s)MemoryPeak (MB)
dir()/read()49993
find/dir135109
glob()51095
scandir()1043113
opendir()/readdir()50794
DirectoryIterator62288
  • Den selben Ubuntu Server, aber mit PHP 5.3.1
MethodeLaufzeit (s)MemoryPeak (MB)
dir()/read()16056
find/dir14676
glob()13158
scandir()291579
opendir()/readdir()14257
DirectoryIterator15657
  • Einmal einen Debian-Server mit QuadCore, Raid1 SATA2 und PHP 5.2.11
MethodeLaufzeit (s)MemoryPeak (MB)
dir()/read()38053
find/dir264
glob()34955
scandir()74464
opendir()/readdir()36654
DirectoryIterator346

Verstehen tue ich einige Ergebnisse noch nicht, zum Beispiel warum die find-Methode auf dem Debian-Server nur 2 Sekunden benötigt, wohingehen sie unter Ubuntu jeweils > 2 Minuten dauert. Das selbe beim DirectoryIterator. Komisch. Außerdem scheint PHP 5.3 ordentlich an Geschwindigkeit zugelegt zu haben gegenüber 5.2.

Falls jemand von euch weiß, wie man die Anzahl der FilesystemEvents (inotify?) unter Linux herausbekommen kann, oder die Anzahl der maximalen gleichzeitigen Filehandles (sowas wie lsof -p PID, nur das Maximum während eines Programmdurchlaufs), bitte in den Kommentaren melden.

Ich überlege auch, ob es sich lohnt, diese Tests mal auf einem NFS-Laufwerk im Netzwerk zu machen, das könnte man evtl. auch mal brauchen.

(Die Tabellen wollte ich übrigens mal ausprobieren, sind mit dem WordPress-Addon WP-Table Reloaded erstellt worden)

Written by Michael Kliewe

Februar 16th, 2010 at 9:29 am

6 Responses to '6 Methoden, ein Verzeichnis rekursiv zu scannen'

Subscribe to comments with RSS or TrackBack to '6 Methoden, ein Verzeichnis rekursiv zu scannen'.

  1. Dein Sortier-Skript hat nen Fehler, oder ist 1043 kleiner als 135? Oder ist 312 kleiner als 46?

    Rob

    16 Feb 10 at 10:43

  2. Falls du die Javascript-Sortierung der Tabellen meinst: Ja, war leider ein Fehler, habe nun die Maßeinheiten aus den Zellen entfernt, nun wird es korrekt sortiert.

    Michael Kliewe

    16 Feb 10 at 10:50

  3. Für PHP gilt – zumindest soweit ich das ausgemessen habe – das auf closedir($handle) verzichtet werden kann. Das script läuft schneller (!) und verbraucht weniger Speicher (!) ohne.

    Auch gibt es kein limit auf Handles. Das hatte ich eigentlich erwartet.

    Mot

    1 Mrz 10 at 20:44

  4. Du sagtest, glob() würde Dateien mit einem Punkt am Anfang, z.B. .htaccess, nicht zurückliefern. Allerdings gibt es hier eine Möglichkeit, diese doch anzeigen zu lassen und zwar mittels glob(‚dir/{,.}*‘, GLOB_BRACE). Damit werden dann aber auch die Einträge „.“ und „..“ mitgeliefert, welche man wiederum rausfiltern müsste.

    Flo

    7 Mrz 10 at 00:02

  5. […] mal hier, in dem Blogpost hat der Autor das hübsch verglichen. Kannste ja mal selbst testen, was bei dir auf dem Server am […]

  6. In vielen PHP-Frameworks sind solche Funktionen (wie hier das rekursive traversieren) bereits ausprogrammiert – z.B. hab ich für rexo folgendes gefunden:

    http://knowledge.rexo.ch/index.php/2113/php-verzeichnis-rekursiv-durchsuchen-wie?show=2114#a2114

    Deine Vergleiche betr. Performance sind trotzdem interessant.

    Hans

    13 Apr 17 at 13:28

Leave a Reply

You can add images to your comment by clicking here.