Ordnung schaffen
Mit wachsender Größe und Verschachtelung eines Web-Angebotes fällt es Besuchern und Web-Masterinnen immer schwerer, den Überblick zu wahren. Es fehlt ein Inhaltsverzeichnis. Der richtige Job also für ein Perl-Skript.
- Axel Köhler
Manuell sind Inhaltsverzeichnisse aufwendig zu erstellen und schlecht zu warten. Besonders, wenn es mehr als eine Verantwortliche gibt, ist das Aktualisieren eine Sisyphusarbeit. Die automatische Erzeugung von Inhaltsverzeichnissen ist bei Sites unproblematisch, die ihre Seiten aus Datenbanken generieren. Diese Systeme sind meist jedoch nicht billig und setzen voraus, daß sie alle zu erfassenden Seiten verwalten. Sie sind also für das schnelle Erstellen eines Index nicht geeignet. Als Lösung bot sich ein kleines Perl-Skript an, das mit wenig Aufwand ansprechende Ergebnisse liefert. Der Aufruf
mk_index.pl startverzeichnis > ergebnis.html
erzeugt das gewünschte Verzeichnis in ergebnis.html. Interessieren nur Dateien nach einem bestimmten Zeitpunkt, kann man ein maximales Alter in Tagen übergeben. Hat sich die Datei in dieser Zeit geändert, nimmt das Skript sie ins Inhaltsverzeichnis auf.
mk_index.pl -t Tage startverzeichnis > ergebnis.html
Ich setze dieses Skript nach jeder Änderung unseres WWW-Angebots ein, bei weniger übersichtlichen Servern bietet sich eine automatische Ausführung zu bestimmten Zeiten an, zum Beispiel über cron. Der Aufbau des Skripts ist einfach. Das Programm
- durchsucht einen Verzeichnisbaum nach HTML-Dateien,
- analysiert den Header und findet den Titel heraus,
- sortiert die Dateien
- und generiert ein HTML-Inhaltsverzeichnis.
LISTING 1
mk_index.pl erzeugt aus allen HTML-Dateien in einem Verzeichnisbaum ein Inhaltsverzeichnis im HTML-Format.
1 #!/usr/bin/perl -w
2
3 use Getopt::Std; # Für Optionen
4 use HTML::HeadParser; # HTML-Parser
5 use HTML::Entities; # <, > et cetera kodieren
6 use File::Find;
7 use strict;
8
9 # Auswertung der Kommandozeile
10 getopts("t:") || die "Wrong
parameter.\n"; ;
11
...
23 # Liste der Refenzen auf die Dateien anlegen
24 my(@lrFiles) ;
25
26 # find für Analyse der HTML-Dateien
27 File::find(\&fc, $main::startdir );
...
37 # Liste der Listreferencen bearbeiten
38 my($listref,$element,$OldDeep,$i);
39 $OldDeep = ($main::startdir =~ tr@/@@);
40
41 # Liste der URLs alphabetisch sortieren
42 my(@liste) = sort my_sort @main::lrFiles;
43
44 # Erzeugen der eigentlichen Liste der Urls
45 # Je tiefer, desto mehr wird eingerückt
46
47 foreach $listref ( @liste) {
48
49 my($Deep,$url,$titel) = @$listref;
50 if ($Deep > $OldDeep) {
51 for ($i =1;$i <= ($Deep - $OldDeep);$i++){
52 print " " x $Deep . "<ul> \n";
53 }
54 }
55 if ($Deep < $OldDeep) {
56 for ($i =1;$i <= ($OldDeep - $Deep);$i++){
57 print " " x $Deep . "</ul> \n";
58 }
59 }
60
61 # Markiere Dokumente ohne Titel
62 $titel = "Kein Titel"
63 unless (defined $titel && length($titel));
64 # Sonderzeichen im Titel schützen
65 # und Eintrag ausgeben
66 encode_entities($titel);
67 print " " x $Deep .
68 " <li> <a href=\"$url\"> $titel </a> \n";
69 $OldDeep = $Deep;
70
71 }
...
80
81 # HTML-Dateien finden, Tiefe im Verzeichnis
82 # und Titel feststellen
83 sub fc {
84
85 my($Path) = $main::startdir;
86 my($FileName) = $_;
87
88 return unless -f; # no directories
89 # Nur Dateien vom Typ *.htm und *.html
90
91 return unless ($FileName =~ /.*\.html?$/ ) ;
92
93 if(defined $main::opt_t) {
94 return if ($main::opt_t < (-M $FileName) );
95 }
96
97 # URL zusammenbasteln
98 my $Url = $File::name;
99 # der Aufrufpfad wird ersetzt
100 $Url =~ s@$Path/@@;
101
102 #Tiefe des Baumes feststellen
103 my $tiefe = $Url =~ tr@/@@;
104
105 # Header analysieren
106 my $p = HTML::HeadParser->new();
107 my $html = $p->parse_file($FileName);
108 # Titel und Ergebnis auf Liste
109 # von Listenrefenzen schieben
110 push(@main::lrFiles,
[$tiefe,$Url,$html->header('Title')] );
111 }
...
115 sub my_sort {
116 return(@$a[1] cmp @$b[1]);
117 }
Dazu bedient sich mk_index.pl der Module File::Find, Getopt::Std und HTML::HeadParser. Ab Zeile 10 wertet Getopt::Std die übergebenen Parameter aus und überprüft sie. Um die Erstellung von Inhaltsverzeichnissen neuer und geänderter Seiten zu ermöglichen, kann man mit -t Tage eine maximale Zeitdauer seit der letzten Änderung der Datei übergeben.
Zeile 24 definiert die später benötigte Liste der Referenzen auf die Dateien (@lrFiles). Der Präfix lr weist auf den Variablentyp hin - typenlose Skriptsprachen haben manchmal auch Nachteile.
Perls Variante von find
Dann folgt der Aufruf der Routine zum Scannen des Verzeichnisbaums
File::find(\&fc, $main::startdir ).
Neben dem Startverzeichnis bekommt die Methode find des File::Find-Moduls die Referenz auf die Subroutine fc übergeben, die es für jede gefundene Datei ausführt. Die dabei ermittelten Daten gibt sie über die globale Liste @lrFiles zurück, da die Subroutine keine Daten über Funktionsparameter austauschen kann.
Als erstes prüft fc() (Zeile 83), ob eine HTML-Datei vorliegt. Hier dient dazu die Kontrolle der Dateiendung. Ist man nicht sicher, daß alle in Frage kommenden Dateinamen mit html oder htm aufhören, müßte das Skript einen Blick in die Datei selbst riskieren. Handelt es sich um HTML-Code, prüft fc() bei gesetzter Option -t, ob ihr Inhalt sich in der vorgegebenen Zeit geändert hat. Anschließend beginnt die eigentliche Verarbeitung: Der später benötigte relative URL läßt sich bestimmen, indem aus dem absoluten Pfad der untersuchten Datei der Weg bis zum übergebenen Startverzeichnis entfernt wird. Dazu zählt die tr-Funktion die Schrägstriche im Pfad. Die Analyse des Dateikopfes erfolgt, indem man mit
HTML::HeadParser->new()
ein neues Parserobjekt generiert, das dann mit der Methode parse_file die nötigen Informationen gewinnt. header("Title") gibt den Titel des Files zurück. Dieser landet zusammen mit den anderen benötigten Angaben als anonyme Liste auf der globalen Liste der Listreferenzen (Zeile 110).
Sind alle Dateien im Verzeichnisbaum verarbeitet, beendet sich die find-Routine. Zurück im Hauptprogramm, schreibt das Skript den Header des Verzeichnisses. Dies geschieht im Programm und nicht durch das Einbinden einer Textdatei, damit Elemente wie der Titel oder eine Datumsangabe programmgesteuert erstellbar sind. Eine eigene Vergleichsroutine my_sort() sorgt dann in
my(@liste) = sort my_sort @main::plFiles;
für die Sortierung der Dateien nach dem URL. Wie in C oder Tcl/Tk kann man in Perl ein eigenes Unterprogramm definieren, das die Sortierreihenfolge von zwei Werten definiert. In der einfachsten Version tut die Routine my_sort (Zeile 115) nicht viel mehr, als aus den beiden übergebenen Argumente $a und $b den Pfad herauszufiltern und dem Vergleichsoperator zu übergeben. Für diese knapp vier Zeilen wäre eine Subroutine eigentlich unnötig, da dasselbe auch mit
my(@liste) = sort { @$a[1] cmp @$b[1]} @main::plFiles;
erreichbar wäre. Lediglich die leichtere Erweiterbarkeit eines Unterprogramms rechtfertigt sie.
Als Sortierung ergibt sich eine reine ASCII-Reihenfolge:
/interessant/band.html/interessant/index.html
/interessant/mueller.html
/interessant/naja/abrakadabr.html
/interessant/naja/index.html.
Diese sortierte Liste gibt das Skript jetzt in einer Schleife aus. Für ein strukturiertes Inhaltsverzeichnis stehen als Information nur der Titel und die Tiefe im Verzeichnisbaum zur Verfügung. Die Tiefeninformation dient zum Erzeugen der HTML-Liste.
Befindet sich eine Datei weiter unten im Verzeichnisbaum als die vorhergehende, schreibt das Skript entsprechend viele HTML-Listenanfänge. Liegt sie höher, erzeugt es Listenenden. Damit diese Einrückung nicht nur später im Browser-Fenster, sondern auch im HTML-Code sichtbar ist, fügt es entsprechend viele Leerzeichen ein. Einen fehlenden Titel ersetzt das Skript durch den Hinweis ‘Kein Titel’. encode_entities() sorgt dafür, daß keine Sonderzeichen aus dem Titel (<, > et cetera) in der neuen HTML-Datei landen. Das ist nötig, da HTML::HeadParser den Titel bereits dekodiert zurückgibt, also mit ‘<’ statt ‘<’. Als letztes wird der HTML-Footer geschrieben.
Eine naheliegende Erweiterung besteht darin, den Namen des Autors und etwaige Beschreibungen des Inhaltes zum Inhaltsverzeichnis hinzuzufügen. Dafür müßte man HTML::Parser einsetzen. Denkbar wäre zudem das automatische Erstellen eines gemeinsamen Inhaltsverzeichnisses für mehrere übergebene Verzeichnisbäume.
Index-Dateien an den Anfang
Häufig existiert in jedem Verzeichnis die Datei index.html. Sie liefert der WWW-Server automatisch, wenn im URL nur das Verzeichnis angegeben ist. Meist enthält diese Datei Verweise auf den Verzeichnisinhalt und dient so als erste Anlaufstelle. Daher ist es wünschenswert, index.html nach vorne zu sortieren, etwa so:
/interessant/index.html
/interessant/band.html
/interessant/mueller.html
/interessant/naja/index.html
/interessant/naja/abrakadabr.html.
LISTING 2
1 # ULRs sortieren, so dass Index-Dateien nach vorne kommen
2 sub my_sort {
3 my $url1 = @$a[1];
4 my $url2 = @$b[1];
5
6 my ($wo,$nr);
7 $url1 =~ /(?=\/index\.html?)/g;
8 $url2 =~ /(?=\/index\.html?)/g;
9 my $wo1 = pos $url1;
10 my $wo2 = pos $url2;
11 $wo1 = 0 unless (defined $wo1);
12 $wo2 = 0 unless (defined $wo2);
13
14 # Normal sortieren, wenn keine Index-Datei dabei
15 unless ($wo1 || $wo2) {
16 return ($url1 cmp $url2);
17 }
18
19 $wo = $wo1 $wo2 ? $wo1 : $wo2;
20
21 # Falls beide die gleiche Anfangswurzel haben
22 # wird das index File als kleiner zurueckgegeben
23 if (substr($url1,0,$wo) eq substr($url2,0,$wo)) {
24 return $wo2 - $wo1 ;
25 }
26 return ($url1 cmp $url2);
27 }
Dies leistet die erweiterte Sortiervergleichsroutine (Listing 2). Da einige Fälle zu unterscheiden sind, ist sie etwas kompliziert ausgefallen. Zunächst sucht sie in $a und $b nach /index.html? (Zeilen 8 und 9). Ein regulärer Ausdruck ist hier flexibler als beispielsweise rindex(). Für den späteren Vergleich ist allerdings die Position der Fundstelle innerhalb des Dateinamens wichtig. Diese liefert pos, wenn man vorher beim Suchen den Schalter g benutzt hat.
Kommt der Name gar nicht vor, gibt my_sort sofort das übliche Sortierresultat zurück (Zeile 16). Gehören beide Dateien zum selben Zweig des Verzeichnisbaums, schiebt es die Indexdatei in Zeile 23 weiter nach vorne. Dafür bestimmt die Routine zunächst die Länge des gemeinsamen Pfades ($wo)). Stimmen die ersten $wo-Zeichen von $url1 und $url2 überein, liegen sie in demselben Verzeichnis. Dann kann aber nur eine von beiden Dateien der Index sein, so daß $wo2-$wo1 der passende Rückgabewert ist: Enthält $url1 den Index, ist $wo2 Null, und -$wo1 sorgt dafür, daß es vor $url2 erscheint. Umgekehrt stellt return $wo2 sicher, daß $url2 vor $url1 einsortiert wird. Gehören die beiden Dateien zu verschiedenen Verzeichnissen, liefert my_sort in Zeile 26 schlicht das normale Sortierergebnis.
AXEL KÖHLER
arbeitet als wissenschaftlicher Mitarbeiter am Institut für Arbeitslehre der TU Berlin.
Benötigte Module
File::Find und Getopt::Std sind in der Perl-Distribution enthalten. Ersteres implementiert Unix’ find Kommando, Getopt::Std kümmert sich um die Auswertung von Optionen auf der Kommandozeile.
HTML::HeadParser und HTML::Entities sind im Paket HTML::Parser enthalten, auf dem CPAN in modules/by-module/HTML zu finden. HeadParser ist eine spezielle ‘leichte’ Version des kompletten Parsers-Moduls für den Kopf-Bereich einer HTML-Datei. Entities stellt Methoden zum Kodieren und Dekodieren von Nicht-ASCII-Zeichen und HTML-Symbolen (<, > et cetera) bereit.
Zusützlich muß die libwww-perl Bibliothek installiert sein, die es im CPAN-Verzeichnis modules/by-module/libwww gibt.
Deutsche CPAN-Spiegel sind unter anderem
ftp://ftp.uni-hamburg.de/pub/soft/lang/perl/CPAN/
ftp://ftp.rz.ruhr-uni-bochum.de/pub/CPAN/
ftp://ftp.leo.org/pub/comp/general/programming/languages/script/perl/CPAN/
(ck)