Verzeichnis-Baum

Arbeit mit Perl im Datei-Verzeichnis

Perl wird oft verwendet, um Arbeiten im 'Verzeichnisbaum' eines Dateisystems auszuführen. Das liegt besonders nahe, je mehr Texte analysiert und verarbeitet werden sollen (Reguläre Ausdrücke). Hier wird einer von vielen Wegen demonstriert, wie ein derartiges Script anpassungsfähig aufgebaut werden kann.
Perl Perl Scripts für CGI und Systemverwaltung
Aufbau Grund-Elemente des Scripts tree_processor
Vorgaben Festlegung der einstellbaren Vorgaben
Initialisierung Ausführung der Arbeiten vor Beginn der 'Arbeitsschleife'
Verzeichnis Bearbeitung eines Verzeichnisses (directory) mit Rekursion
Datei Bearbeitung einer Datei (file)
Statistik Auswertung gesammelter Daten
Anregungen Typische Anwendungen dieser Arbeitstechnik
Beispiel Komplettes Demo-Programm
Perl Module Zusatz-Module für die Verarbeitung von Verzeichnis-Bäumen

Aufbau von tree_processor

Aufgaben dieses Demo-Scripts:
Durchlaufe sämtliche Dateien und Unterverzeichnisse eines angegebenen Pfades.
Führe dabei mit jedem Verzeichnis und mit jeder Datei bestimmte Aufgaben aus.

Das Script soll anpassungsfähig sein: Die Aufgaben sollen leicht austauschbar sein.
Der Aufwand für Layout, Formatierung und zum Abfangen von Fehlern wird im Interesse der Übersichtlichkeit auf ein Minimum begrenzt.
Block-Liste:
Festlegung der Vorgaben
Initialisierung
Bearbeitung des Start-Verzeichnisses
Weitere Arbeit je nach den dabei gefundenen Unter-Verzeichnissen und Dateien
Ausgabe allfälliger Ergebnisse (Statistik)

Die Details zu jedem Arbeits-Block sowie ein komplett lauffähiges ↓ Muster-Programm finden sie in den Kapiteln dieser Seite.

Festlegung der Vorgaben

Start-Verzeichnis:

Dieses Script benötigt als Mindest-Vorgabe nur die Festlegung des Start-Verzeichnisses.
Wählen sie zur Entwicklung eigener Scripts zunächst ein Test-Verzeichnis mit wenigen Dateien.
# Vorgabe: Arbeits-Pfad
my($start_path)='/home/test'; # Linux
my($start_path)='c:/test';   # Windows

Filter:

Meistens werden nicht alle gefundenen Dateien verarbeitet, sondern eine Auswahl daraus getroffen. In diesem Beispiel wird nach Datei-Erweiterung gefiltert, diese stellt daher eine weitere Vorgabe dar.
# Vorgabe: Datei-Erweiterung
my($filter_xtn)='doc';

Runtime-Argumente

Beim Aufruf (Programm-Start an der Konsole) kann man nach dem Programm-Namen Argumente eingegeben (durch Leerzeichen getrennt).
Wenn Argumente vorhanden sind, werden die Vorgaben damit überschrieben, ansonsten gelten die festgelegten Standard-Werte.
# Argumente (überschreiben die Vorgaben)
if($#ARGV>=0) {$start_path=$ARGV[0];}
if($#ARGV>=1) {$filter_xtn=$ARGV[1];}

Details zu Arrays und bergabe von Argumenten im Array @ARGV

Profi-Variante:

Weitere typische Vorgaben zur Steuerung des Programms:
Schalter zur wahlweisen Ausgabe als Text oder HTML
Schalter zur wahlweisen Ausgabe an der Konsole, an den Webserver (dynamische Webseite) und / oder an eine LOG-Datei
Schalter zur Steuerung des LOG-Umfangs: In die LOG-Datei können die Daten knapp oder ausführlich und umfangreich eingetragen werden.
Pfad zur LOG-Datei.
Wenn die Anzahl der Argumente zur Programm-Steuerung groß ist, dann verwendet man dafür eine kleine Text-Datei (Konfigurations-Datei), aus welcher das Programm seine Vorgaben liest.

Debug:

Zumindest in der Entwicklungs-Phase braucht man an allen kritischen Programm-Stellen Ausgabe-Befehle.
Steuern sie die Debug-Ausgabe mit einem Schalter (hier $debug )
In diesem Fall werden die Ausgabe-Befehle nicht mehr entfernt und können auch später beliebig ein- oder ausgeschaltet werden:
# Vorgabe für DEBUG: ein/aus -> 1/0
my $debug = 1;
# Anwendung
if($debug) {warn 'Debug-Ausgabe';}
Wenn sie zur Ausgabe warn anstatt print verwenden, dann wird die aktuelle Programm-Zeile ausgegeben. Schließen sie die Meldung ohne \n ab.

Initialisierung vor Beginn der 'Arbeitsschleife'

Zähler und Speicher:

Je nach Aufgabe werden im Hauptprogramm (main) alle verwendeten Zähler (skalare Variable) und Speicher (Arrays und Hashes) deklariert, die Zähler mit Anfangs-Werten initialisiert, z.B.
Anzahl der durchlaufenen Verzeichnisse dir_count
Anzahl der gefundenen Dateien file_count
Anzahl der bearbeiteten Dateien files_processed
Summe der Dateigrößen (gesamt oder bearbeitet) file_size
usw.
Details zu Perl - Arrays und Hashes.
# Initialisierung
my $dir_count = 0;
my $file_count = 0;
my $files_processed = 0;
my file_size = 0;

LOG-Datei:

Bei Bearbeitung von Datei-Bäumen (trees) werden oft zig-tausende Verzeichnisse und Dateien durchlaufen. Das verbietet die Ausgabe der Ergebnisse auf der Konsole oder auf Webseiten. Text-Dateien sind dazu ideal geeignet: Sie werden rasch und unproblematisch erstellt oder ergänzt. Darüber hinaus können sie durch andere Programme gelesen und weiter verarbeitet werden.

Zugriffsrechte
Das Schreiben von LOG-Dateien erfordert im betreffenden Verzeichnis Schreib-Rechte (je nach System und Anwendung für den ausführenden User, für den Perl-Interpreter oder für den Webserver). Schreib-Rechte sind immer ein Sicherheits-Risiko. Schreiben sie daher die Log-Datei am besten in ein eigenes Verzeichnis: Dort können sie sogar alle Rechte freigeben - Im schlimmsten Fall kann nur die LOG-Datei geändert oder gelöscht werden.
Erzeugung einer Log-Datei:

$logpath = "/var/log/tree/tree.log";
open(LOG, ">$logpath") or die;
my $t=strftime("%Y-%m-%d %H:%M:%S",localtime(time()));
print LOG "# $t\n";


Voraussetzung: Schreib-Rechte im Ziel-Verzeichnis ! Die Eintragung von Datum und Zeit bewährt sich immer.
Details zum Schreiben von Text-Dateien bzw. zu Datum und Zeit
Arbeits-Beginn
Die Vorbereitungen nehmen den größten Teil des Hauptprogramms ein. Die gesamte Arbeit erfolgt in der Programmzeile rechts. Das Sub process_dir wird im nächsten Kapitel vorgestellt.

process_dir($start_path);

Bearbeitung eines Verzeichnisses

Zur Verarbeitung beliebiger Verzeichnisse (Ordner, folder) wird ein eigenes sub (Perl-Unterprogramm) verwendet.
Die einzige Aufgabe besteht darin, alle darin enthaltenen Objekte nach Typ (Verzeichnis oder Datei) zu erkennen und getrennt zu verarbeiten.
Unter-Verzeichnisse werden selbst wieder mit dem gleichen sub verarbeitet - Dieses muss sich daher selbst aufrufen. Diese Methode wird rekursiv genannt.
Vereinfachtes Muster zur Verarbeitung eines Verzeichnisses:

Argumente:
An dieses Sub wird nur 1 Argument übergeben, der Pfad zum Verzeichnis. Das Argument wird aus dem Standard-Array @_ übernommen und an die lokale (private) Variable $path übergeben.

Verzeichnis:
Mit opendir wird das angegebene Verzeichnis geöffnet. Für jeden weiteren Zugriff auf dieses Verzeichnis wird ein directory-handle (hier $dh ) benötigt. Als Handle wird in einfachen Fällen (so wie bei → Dateien) ein Name wie z.B. DIR verwendet. Das ist hier unzulässig, weil sich das Sub selbst oftmals (rekursiv) aufruft, und daher jedesmal ein neues Handle erzeugt werden muss. Daher wird das Handle mit einer lokalen Variablen (hier $dh ) angelegt.

Initialisierung:
Alle in der Arbeits-Schleife verwendeten lokalen Variablen werden vor Beginn der Schleife deklariert.

Arbeits-Schleife:
Die while-Schleife läuft mit readdir über alle im geöffneten Verzeichnis enthaltenen Objekte $obj
Die Objekte . und .. werden übersprungen. Sie sind in jedem Verzeichnis enthalten und werden auf der Konsole normalerweise nicht angezeigt.
Für jedes Objekt wird der Pfad $objpath zusammengestellt.

Verzeichnis oder Datei ?
Mit if(-d) wird getestet, ob es sich beim untersuchten Objekt $obj um ein Verzeichnis (Ordner, directory) handelt.
Ein (Unter)-Verzeichnis wird mit dem gleichen Sub verarbeitet. Nach Rückkehr wird mit next das nächste Objekt untersucht.
Wenn das untersuchte Objekt eine Datei ist, dann wird sie mit process_file weiter verarbeitet.
sub process_dir {
my ($path) = @_;
$dir_count++;

# Verzeichnis öffnen
if (!opendir(my $dh,$path)) {return;}
# Initialisierung
my($obj,$objpath);

# Schleife über alle Objekte
while ($obj = readdir($dh)) {
# skip up-directory
if ($obj =~ /^\.{1,2}$/) {next;}
$objpath = $path.'/'.$obj;
if (-d $objpath) {
# Unterverzeichnis
process_dir($objpath);
next;
}
# Datei
process_file($objpath);
}
}

Dieses Sub arbeitet als Verteiler: Alle im untersuchten Verzeichnis $path enthaltenen Unter-Verzeichnisse werden mit dem gleichen Sub process_dir (rekursiv) verarbeitet, alle Dateien mit einem anderen Sub process_file
Als einzige hier anfallenden Daten wird die Anzahl der verarbeiteten Verzeichnisse $dir_count erhöht.

Bearbeitung einer Datei

Zur Verarbeitung beliebiger Dateien (files) wird ein eigenes sub (Perl-Unterprogramm) verwendet.
Alle auszuführenden Arbeiten werden hier eingetragen.
Dieses Sub unterscheidet sich naturgemäß je nach der gestellten Aufgabe. Hier wird ein ganz einfaches Beispiel gezeigt.
Vereinfachtes Muster zur Verarbeitung einer Datei:

Argumente:
An dieses Sub wird nur 1 Argument übergeben, der Pfad zur Datei. Das Argument wird aus dem Standard-Array @_ übernommen und an die lokale (private) Variable $path übergeben.
In jedem Fall wird die Anzahl der untersuchten Dateien $file_count erhöht.

Filter
Die Datei (Name, Größe, Alter, ...) wird kurz analysiert. Danach wird entschieden, ob und wie diese Datei weiter zu bearbeiten ist.
Das Filter dieses Beispiels untersucht z.B. nur *.htm Dateien und ignoriert alle anderen Dateien.

Verarbeitung
Die gesamte Verarbeitung beschränkt sich hier darauf, die Datei-Größe zu ermitteln und in der globalen Variablen $file_size zu summieren. In der Praxis können sie an dieser Stelle beliebig umfangreiche Arbeiten ausführen lassen.
sub process_file {
my($path) = @_;
$file_count++;

# Filter
my $fx = get_file_extension($path);
if($fx eq "htm") {

# Verarbeitung
$files_processed++;
$file_size+=get_filesize($path);
print LOG "size($path)=$file_size\n";
}
}

Bei Bedarf werden von diesem Sub Verarbeitungs-Daten in die LOG-Datei geschrieben.
Das Sub get_file_extension entnimmt dem Dateinamen die 'Extension', die zwar nur für Windows benötigt wird, aber aus Gründen der Kompatibilität allgemein verwendet wird. Das Sub isoliert mit einem → Regulären Ausdruck entweder alle oder 2..5 Zeichen nach dem letzten Punkt im Datei-Pfad oder -Namen.
sub get_file_extension {
my($myfn) = @_;
$myfn =~ /([^\.]*)$/;
$myfn =~ /([^\.]{2,5})$/;
return $1;
}
Das Sub get_filesize dient hier als Beispiel dafür, wie sie mit stat weitere Eigenschaften einer Datei erfassen können.
Hier wird lediglich die Datei-Größe in Byte zurückgegeben.
sub get_filesize {
my($path) = @_;
my($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $therest) = stat($path);
return $size;
}

Auswertung gesammelter Daten (Statistik)

Nach Abschluss aller Arbeiten wird meist eine Statistik der verarbeiteten Verzeichnisse und Dateien ausgegeben bzw. in die LOG-Datei geschrieben.
print "$dir_count Verzeichnisse\n";
print LOG "dir_count=$dir_count\n";
print "$file_count Dateien\n";
print LOG "file_count=$file_count\n";
print "$files_processed Treffer\n";
print LOG "files_processed=$files_processed\n";
$t=strftime("%Y-%m-%d %H:%M:%S",localtime(time()));
print LOG "# $t\n";

Anregungen

Einige typische Beispiele für die Anwendung der gezeigten Methode 'Durchforstung von Datei-Bäumen'

Zählung, Statistik:

Zählung aller oder ausgewählter Verzeichnisse und Dateien. Abbildung von Verzeichnis-Bäumen in einer Datenbank, z.B. Eintragung aller gefundenen Bilder mit Name, Art, Größe, Datum usw. in eine Bild- oder Medien-Datenbank. Zusätzlich zur reinen Zählung kann z.B. sortiert werden. Auf diese Weise können z.B. Dubletten (exakte Kopien von Dateien) aufgespürt werden.

Dateinamen:

Systematische Änderungen an den Namen von Verzeichnissen und Dateien, z.B. Entfernen führender oder folgender Leerzeichen, Ersatz von Sonderzeichen (äöüß), Ersatz aller *.JPG, *.JPEG und *.jpeg durch *.jpg usw.

Datei-Text

Text-Dateien (z.B. *.htm, *.html, *.php) werden geöffnet und der Quelltext analysiert.
Auswertung für Statistik (Anzahl der enthaltenen Bilder, ...), als Crawler (alle Links werden herausgesucht und gespeichert) oder Speicherung bestimmter darin enthaltenen Texte für einen Stichwort-Index.
Link-Checker: Man kann eigene Webseiten danach durchsuchen, ob Links zu Seiten darin vorkommen, die nicht mehr existieren, oder die umbenannt wurden.

Mit kleinen Änderungen kann diese Methode auch zur systematischen Änderung von Texten verwendet werden: Perl kann tausende von Dateien nach bestimmten Text-Mustern durchsuchen und sie durch andere Texte ersetzen (Vorsicht - vorher Sicherungskopie anlegen !).
Dabei kann man durchaus komplexe Regeln aufstellen, also Textmuster nur in einem genau eingegrenzten Zusammenhang ändern.

OpenDocument

Die Verwendung des standardisierten OpenDocument Formats (z.B. für Text-Dateien, Briefe, ...) hat große Vorteile gegenüber den Formaten (z.B. *.doc) des IT-Marktführers: OpenDocument-Dateien sind reine → XML Text-Dateien und können daher von entsprechenden Programmen (z.B. Perl-Scripts) gelesen und auf Wunsch geändert werden. Die Dateien sind allerdings in Archive (*.zip) gepackt und müssen daher vor ihrer Verarbeitung ausgepackt und danach wieder komprimiert werden.
Die Verwendung von → OpenOffice an Stelle der Office-Pakete des Marktführers hat viele Vorteile, u.a.
OpenOffice ist kostenlos erhältlich und kann aus dem Internet geladen werden.
OpenOffice gibt es für alle gängigen Betriebssysteme. Es ist Bestandteil aller gängigen Linux-Distributionen.
Mit OpenOffice kann man sowohl die Dokumente des Marktführers (*,doc, *.xls, ...) öffnen, bearbeiten und speichern als auch OpenDocument-Dateien. Mit der Software des Marktführers ist das nicht möglich.
OpenDocument wird zunehmend von großen Unternehmen und Behörden eingesetzt, um die Abhängigkeit von einem einzelnen Hersteller abzubauen.

Entfernte PC

Die Verarbeitung von Dateien ist nicht auf den eigenen PC beschränkt:
Ein Perl-Script kann Dateien auf allen erreichbaren PC durchforsten, z.B. im lokalen Netzwerk oder im Internet.
Ein Crawler-Script sucht beispielsweise im Internet nach Webseiten mit bestimmten Stichworten oder Wort-Kombinationen, und folgt dabei allen oder ausgewählten Links in den untersuchten Webseiten. Die Ergebnisse werden meist bewertet und geordnet, und in einer Datenbank gespeichert.
Perl-Vorteile Wenn es um Bearbeitung von Verzeichnis-Bäumen und/oder Text-Dateien geht, dann bietet Perl mit Regulären Ausdrücken den derzeit besten Komfort. Je komplexer die Anforderungen, desto mehr kommen die Vorteile zum Tragen.
Perl-Scripts lassen sich ideal automatisieren, zeitlich gesteuert auslösen (→ Linux, → Windows), können sich gegenseitig mit Daten versorgen und nach Bedarf aufrufen. Man kann sogar Scripts erstellen, die selbst Script-Dateien schreiben und danach aufrufen ...
Perl-Nachteile Nachteil von Perl: Als Script-Interpreter-Sprache ist es relativ langsam, daher weniger für zeitkritische Anwendungen geeignet.
Kleinere Scripts können auch auf einem Webserver laufen, z.B. im lokalen Netzwerk: Perl-Scripts auf Apache Webserver werden compiliert und für gewisse Zeit gespeichert, d.h. sie laufen bei erneutem Aufruf genauso schnell wie Programme in Maschinensprache.
Modul File
Dieses Modul bietet einige Funktionen, welche das Durchforsten eines ganzen Verzeichnisbaums sehr vereinfachen.

use File::CheckTree;
use File::Find;
Modul HTML
Dieses Modul bietet einige Funktionen, die auf die Verarbeitung von HTML-Quellcode-Dateien spezialisiert sind.

use HTML::Entities;
use HTML::Parser ();
Funktion glob
Diese Funktion bietet umfangreiche Möglichkeiten zur Arbeit in einem Dateisystem. Sie liefert die Namen aller Objekte im aktuellen Verzeichnis oder in einem angegebenen Verzeichnis als → Array.
Für Verzeichnis- oder Datei-Namen können auch Platzhalter * (beliebig viele Zeichen) oder ? ( 1 Zeichen) verwendet werden. Die Beispiele zeigen, wie Dateien (@f_ar) oder Verzeichnisse (@dir_ar) zurückgegeben werden.
Zur weiteren Verarbeitung ist die Funktion grep gut geeignet, welche das Array mit Regulären Ausdrücken filtern kann.
Die Funktion readdir (im Beispiel process_dir auf dieser Seite verwendet) liefert alle Objekte (Verzeichnisse und Dateien) für ein Directory-Handle, welches vorher mit opendir erzeugt wurde.

my (@f_ar, @dir_ar, @filt_ar, @obj_ar);
@f_ar = glob("c:/test/*.doc");
@f_ar = glob("c:/test/*/*.*");
@f_ar = glob("c:/test/*/?????.xml");
@dir_ar = glob("c:/test/*/");

@filt_ar = grep(/html|htm/, @f_ar);

opendir(my $dh, "c:/test");
@obj_ar = readdir($dh);

Programm-Beispiel

Dieses Beispiel fasst alle oben demonstrierten Einzelheiten zusammen. Es lässt sich als Muster für eigene Programme zur Bearbeitung eines Verzeichnis-Baums verwenden.
Wie für alle anderen Beispiele gilt: Keine Gewähr, Verwendung auf eigenes Risiko !
Das Programm lässt sich z.B. in einem Konsolen-Fenster (Linux-Shell, Win cmd.exe ) starten (wenn Perl korrekt installiert ist), z.B.:
tree_processor.pl /home/test
tree_processor.pl c:/test
tree_processor.pl c:/test doc
tree_processor.pl c:/myweb htm
Es gibt viele Möglichkeiten, Perl-Programme wie dieses zu starten, z.B.:
Manuell in einem → Konsolen-Fenster (Linux-Shell, Windows-Eingabeaufforderung)
Durch einen Webserver, wenn die entsprechende Webseite (→ CGI) von einem Browser angefordert wurde
Automatisch bei bestimmten Ereignissen (System-Start, System-Shutdown, ..)
Automatisch an bestimmten Tagen, zu bestimmten Zeiten oder nach bestimmten Zeit-Intervallen (→ Linux, → Windows)

Durch → Konsolen-Script-Programme
Durch eigene Programme (C++, Java, Perl, PHP, → VBA, ...)
Durch beliebige andere Programme ...

In einigen dieser Beispiele ist keine Ausgabe erwünscht - In diesem Fall schalten sie z.B. alle print-Befehle mit # Kommentar ab.

Perl-Module

Perl bietet eine große Anzahl zusätzlicher Module mit spezialisierten Funktionen.
Am Server ihres Web-Providers können sie keine Module installieren, für den eigenen Gebrauch können die Module jedoch praktische Funktionen enthalten, die evtl. viel Arbeit sparen.
Beispiele für Stichworte bei der Suche nach nützlichen Modulen:
parse, recursive, robot, tree, ..