Gleichmacherei
The Answer Girl
Nur lokal
Wenn in der Referenz kein Protokoll steckt, versuchen wir, die Datei zu öffnen:
open( DATEI, $datei );
DATEI ist ein sogenanntes Handle, mithin ein Stellvertreter für die Datei, deren Name in $datei steckt. Geht das in Ordnung, haben wir nichts zu berichtigen und können die Datei wieder schließen:
close DATEI;
Geht das Öffnen hingegen schief, …
if ( ! open( DATEI, $datei ) ){}… müssen wir versuchen, den richtigen Dateinamen rauszubekommen. Wenn der in der Linkangabe mit einem / beginnt, bezieht er sich als absolute Pfadangabe nicht auf das Wurzelverzeichnis des Dateisystems, sondern auf die entsprechende Document Root auf dem Webserver. Halten wir also erst einmal das Verzeichnis in einer Variablen fest, das als Startverzeichnis für die Dateien der Website dient:
$rootDir = "/home/pjung/LU/LU1001/answergirl";
Dieser Umstand macht unsere Aufgabe etwas schwerer: Um eine im Link angegebene Datei, die mit / beginnt, zu finden, gilt es, nicht in /, sondern in $rootdir danach zu suchen. Gibt es etwas zu korrigieren, darf jedoch nicht $rootdir/korrigierter_Name zurück in den Link geschrieben werden, sondern lediglich /korrigierter_Name.
Was liegt also näher, als den korrigierten Namen und das Präfix, das wir zum Auffinden der Datei im Dateisystem brauchen, getrennt zu speichern? Setzt man die Inhalte beider Variablen, $praefix und $korrDat mit dem Punktoperator . zusammen, erhalten wir die Dateiangabe maßgeschneidert für's Dateisystem, nehmen wir nur $korrDat, haben wir die Angabe passend für den Link.
Schreiben wir also zunächst ein / in die Variable, die den korrigierten Link enthalten soll:
$korrDat = "/";
Außerdem merken wir uns das Root-Verzeichnis als Präfix:
$praefix = $rootDir;
Mit dieser Vorbereitung für's spätere Zusammensetzen im korrigierten Dateinamen können wir auf den Schrägstrich am Anfang (^) der in $datei gespeicherten Linkangabe verzichten. Um die Bedingung zu formulieren, unter der $korrDat und $praefix wie eben beschrieben gesetzt werden sollen, benutzen wir daher nicht den Match-, sondern den Substitute-Operator s. Wir sagen dem Skript einfach: "Wenn Du am Anfang von $datei einen / durch Nichts ersetzen kannst, setze $praefix und $korrDat wie eben besprochen."
Leider ist / mal wieder ein Fall für das Escape-Zeichen. Doch glücklicherweise gibt es eine Möglichkeit, das zu suchende und das ersetzende Muster nicht nur mit /, sondern auch mit anderen Sonderzeichen voneinander abzutrennen. Nehmen wir zum Beispiel das Dollarzeichen. Damit wird aus "Suche / am String-Anfang, und ersetze es durch Nichts" keine Escape-Orgie, sondern ein einfaches s$^/$$. Um diese Ersetzung gleich in der Variablen $datei vorzunehmen, sagen wir
$datei =~ s$^/$$;
Als Bedingung formuliert, sieht das dann so aus:
if ( $datei =~ s$^/$$ ){ }War der Link kein absoluter, bleibt $korrDat noch leer, und im Präfix wird der Punkt als Stellvertreter des aktuellen Verzeichnisses samt Trenn-Slash gespeichert:
else {
$praefix = "./";
}Häppchenweise
Wenn einmal der Link kaputt ist, kann dies am Dateinamen selbst oder an einem der im Pfad angegebenen Verzeichnisse liegen. Folglich müssen wir in den sauren Apfel beißen und jeden durch / getrennten Bestandteil von der Wurzel bis zur Spitze überprüfen. Dazu teilen wir den um einen eventuellen führenden / beraubten Inhalt von $datei an den Slash-Stellen in kleine Häppchen und speichern sie im Array @teile:
@teile = split( /\//, $datei );
Die split()-Funktion benötigt zwei Argumente: den String, den sie zerhacken soll, und das Trennzeichen. Statt nun einfach einen Delimiter anzugeben, kommt hier der Match-Operator zum Einsatz. Zwischen seine beiden /-Flügel setzen wir den die Verzeichnisse trennenden Slash /, und damit der nicht mit dem rechten Flügel-/ verwechselt werden kann, kommt ein \ davor.
Sukzessive nehmen wir die Teilchen nun unter die Lupe:
foreach $teil ( @teile ){ }Wenn der Pfadbestandteil in $teil ein Punkt für das aktuelle oder () ein Doppelpunkt für das übergeordnete Verzeichnis ist, brauchen wir keine Schreibweise prüfen und fügen den Inhalt von $teil an den String an, der bereits in $korrdat steht:
if ( $teil eq "." || $teil eq ".." ){
$korrDat .= $teil . "/" ;
}Perl kennt zwei Gleichheitsoperatoren: für numerische Werte und für Zeichenketten. Letzterer heißt eq ("equal"). $korrDat .= $teil; wiederum ist eine Kurzschreibweise für
$korrDat = $korrDat . $teil;
Mit dem Anhängeoperator für Zeichenketten, dem Punkt, fügen wir zudem einen Slash als Verzeichnistrenner ein.
Haben wir hingegen einen echten Datei- oder Verzeichnisnamen in $teil stehen, gibt es mehr zu tun. Versuchen wir zunächst, das, was bislang in $korrDat steht, zu verifizieren: Mit dem $praefix davor haben wir es mit einem Verzeichnis zu tun, dass es zu öffnen gilt:
opendir (VERZ, $praefix . $korrDat );
Schließen werden wir es später mit closedir( VERZ );. Sind wir beim Öffnen allerdings nicht erfolgreich, können wir gleich aufgeben und die Bearbeitung der @teile beenden:
opendir (VERZ, $praefix . $korrDat ) || last ;
last verlässt die aktive Schleife, sodass wir mit der Bearbeitung der nächsten $datei weitermachen können.
Wenn wir das Verzeichnis $praefix . $korrDat hingegen öffnen und den Handler VERZ "installieren" konnten, lesen wir am besten mit
readdir( VERZ );
alle darin vorhandenen Dateien aus. Gibt es dort eine Datei oder ein Verzeichnis mit dem Namen, der in $teil gespeichert ist? Auf der Shell würden wir dazu den grep-Befehl benutzen – und netterweise gibt es ihn auch in Perl:
grep ( /$teil/i , readdir( VERZ ) );
Das Muster wird dabei vom Match-Operator umgeben – und natürlich darf die i-Option nicht fehlen, denn die Groß-Kleinschreibung der tatsächlichen Datei kann von $teil durchaus verschieden sein.
Eins haben wir dabei jedoch nicht bedacht: grep findet in dieser Version auch dann Übereinstimmungen, wenn der Inhalt von $teil lediglich Bestandteil eines vorhandenen Datei- oder Verzeichnisnamens ist. Hier gilt es, das Muster zu präzisieren: Wir geben es inklusive Anfang (^) und Ende ($) an und speichern das Ergebnis in einem Hilfsarray:
@auswahl = grep ( /^$teil$/i , readdir( VERZ ) );
Enthält @auswahl nun nichts, also nicht mal ein nulltes Element, können wir keine korrigierte Fassung des Links erstellen und missbrauchen $korrDat für einen HTML-Kommentar, der sagt, dass $datei nicht auffindbar war. Weiter geht's dank last am Schleifenanfang mit einem eventuellen nächsten Element von @dateien:
if ( $#auswahl < 0 ){
$korrDat = ("<!-- " . $datei . " nicht auffindbar! -->" );
last;
}Mit komischen Sonderzeichenkombinationen, die für unfreiwilliges Gedächtnistraining sorgen, ist Perl reichlich gesegnet. Klaut man einem Array das @ und ersetzt es durch ein $#, erhält man ($) eine Skalarvariable, in der (#) die Anzahl der Array-Mitglieder gespeichert ist.
War die in @auswahl gespeicherte Ausbeute etwas zu erfolgreich (wir erinnern uns, verzeichnis und Verzeichnis können auf Unixsystemen problemlos nebeneinander existieren), setzen wir $korrDat auf einen Kommentar, der besagt, dass es mehrere Möglichkeiten gibt:
elsif ( $#auswahl > 0 ){
$korrDat = ("<!-- " . $datei . " nicht eindeutig! -->" );
last;
}Nur wenn wir genau eine Variante finden, können wir das nullte Element von @auswahl an $korrDat anhängen:
else {
$korrDat .= $auswahl[0];
}Bleibt ein trailing slash anzuhängen, um $korrdat bereit für neue Unterverzeichnisebenen zu machen. Zu blöd – wenn $teil nun den Dateinamen enthielt, hat auch der einen Slash am Ende, obwohl es da nicht mehr weiter geht. Doch so weit, wie wir bislang mit unserem Skript gekommen sind – immerhin ist es nur ein Skript, um schnell ein paar Groß-Kleinschreibungsfehler auszubügeln – genehmigen wir uns einen üblen Hack: Wir ersetzen den Slash am Ende von $korrDat mit nichts. Und weil's so schön ist, nehmen wir dazu das Plus als Trennzeichen für den Substitute-Operator:
$korrDat =~ s+/$++;
Achja, jetzt hätten wir beinahe vergessen, die aus der Datei ausgelesene $zeile zu korrigieren, indem wir den alten Link $datei durch die korrigierte Ausgabe $korrDat ersetzen …
$zeile =~ s+$datei+$korrDat+;
… und zu guter Letzt natürlich auszugeben:
print $zeile;
Damit naht der große Augenblick: Ab ins Testverzeichnis und das Skript ohne weitere Parameter, aber vielleicht besser durch less "gepipet" auf die dortigen Dateien loslassen. Sieht gut aus? Dann nehmen wir noch schnell das Kommentarzeichen aus der Zeile
# $^I = ".bak";
heraus, und schon legt cgks die konvertierten Dateien unter ihren alten Namen ab, während die Ursprungsversion als Backupdatei mit der Endung .bak einen Vergleich ermöglicht.
Listing 2
als Ganzes
#!/usr/bin/perl -w
$^I = ".bak";
@ARGV = <*.html>;
$rootDir = "/home/pjung/LU/LU1001/answergirl";
while ( $zeile = <> ){
$_ = $zeile;
@dateien = m/<A HREF=\"(.*?)\">/ig;
foreach $datei ( @dateien ){
$korrDat = "";
if ( ! /(ftp|http):\/\//i ){
if ( ! open( DATEI, $datei ) ){
if ( $datei =~ s$^/$$ ){
$praefix = $rootDir;
$korrDat = "/";
} else {
$praefix = "./";
}
@teile = split( /\//, $datei );
foreach $teil ( @teile ){
if ( $teil eq "." || $teil eq ".." ){
$korrDat .= $teil . "/" ;
} else {
opendir (VERZ, $praefix . $korrDat ) || last ;
@auswahl = grep ( /^$teil$/i , readdir( VERZ ) );
closedir( VERZ );
if ( $#auswahl < 0 ){
$korrDat = ("<!-- " . $datei . " nicht auffindbar! -->" );
last;
} elsif ( $#auswahl > 0 ){
$korrDat = ("<!-- " . $datei . " nicht eindeutig! -->" );
last;
} else {
$korrDat .= $auswahl[0];
}
$korrDat .= "/";
}
}
$korrDat =~ s+/$++;
$zeile =~ s+$datei+$korrDat+;
}
close DATEI;
}
}
print $zeile;
}
Kasten 1: Manöverkritik
Sicher gibt es immer elegantere Möglichkeiten, ein Skript zu basteln. So ließe sich beispielsweise eine bei großen Datenmengen schnellere Variante schreiben, die sich jedes überprüfte Verzeichnis merkt, sodass nicht bei jedem Link jedes Verzeichnis neu betrachtet werden muss.
Viel kritischer für den Einsatz ist jedoch, dass das vorgestellte Programm dann versagt, wenn der HTML-Anker A HREF und die Linkquelle auf verschiedenen Zeilen stehen. Dieses Handicap in Kauf nehmen oder noch mehr Komplexität speziell bei den regulären Ausdrücken einzubauen, war die Alternative.
Für Gelegenheitsperlianer eher unbekannt ist jedoch die Tatsache, dass im "Comprehensive Perl Archiv Network" CPAN jede Menge Module lagern, die ähnlich Bibliotheken bei Sprachen wie C, C++ oder Java Unmengen Funktionen für alle möglichen und unmöglichen Einsatzgebiete enthalten.
Im Fall unseres Skripts hätten wir uns die Arbeit mit den regulären Ausdrücken, um mühselig Links zu erkennen, sparen können, wenn wir das bei vielen Distributionen bereits per Default installierte HTML::Parser-Modul (bei SuSE im Paket perl-HTML-Parser, bei Caldera in perl-modules) benutzt hätten.
Allerdings hat dieses Modul einen großen Nachteil: Wir bewegen uns damit in objektorientierten Perl-Gefilden und dürfen uns damit herumschlagen, was ein Parser ist. Für Perl-Noviz(inn)en und Programmmieranfänger(innen) vermutlich ein etwas zu ambitioniertes Projekt.
Glossar
A HREF
Um in einer Webseite mit einem Hyperlink auf eine andere Datei oder Internet-Ressource zu verweisen, schreibt man einen "Anker" in den Text. Dieser wird um die "Hyperreferenz" ergänzt, die angibt, worauf der Link verweist, z. B. A HREF="http://www.linux-user.de/". Damit diese Angabe für die Leser(innen) der Seite nicht im Text erscheint, klammert man sie in spitze Klammern (<A HREF="http://www.linux-user.de/">). Anschließend folgt der Text, auf den "geklickt" werden soll, um zum Verweis zu gelangen. Zu guter Letzt folgt das Ende-Tag </A>, mit dem der Anker abgeschlossen wird: <A HREF="http://www.linux-user.de/">LinuxUser</A>.
Pfad
Der von Verzeichnissen gepflasterte Weg zu einer Datei. Absolute Pfade beginnen an der durch / gekennzeichneten Wurzelspitze des Dateisystems, relative Pfade im aktuellen Verzeichnis.
Pipes
Eine "Rohrleitung", durch die die Ausgabe eines Kommandozeilenprogramms durchgeleitet wird und am "Leitungsende" als Eingabe für ein zweites Werkzeug dient. Sie wird durch einen senkrechten Strich | symbolisiert: kommando1 | kommando2.
regulären Ausdrücke
Von verschiedenen Standard-Unix-Tools verwendete Möglichkeit, Muster auszudrücken. So steht ein Punkt für ein beliebiges Zeichen oder ein Buchstabe für sich selbst. Folgt ein Sternchen, darf das, was vom vorangegangenen Muster abgedeckt wird, beliebige Male, auch keinmal auftreten. Ein Fragezeichen bedeutet hingegen, dass das, worauf es sich bezieht, genau null- oder einmal vorkommt.
Python
Eine objekt-orientierte Skript-Sprache. Programme, die mit Skript-Sprachen geschrieben werden, müssen nicht extra kompiliert werden, sondern können mit einem Interpreter direkt aus dem Source-Code ausgeführt werden. Oft (z. B. bei Perl) kompiliert zwar der Interpreter ein "internes" Binärprogramm, um die Ausführungsgeschwindigkeit zu erhöhen, doch davon merken Anwender(innen) im Normalfall nichts. Da der Aufruf eines Compilers entfällt, eignen sich interpretierte Sprachen besonders gut für kleine Programme, die "schnell hingeschrieben" eine Aufgabe lösen sollen und gar nicht für den Gebrauch durch Dritte bestimmt sind.
Tcl
Eine Skript-Sprache, die meist im Zusammenhang mit dem GUI-Toolkit Tk zum Schreiben grafischer Anwendungen verwendet wird. Sie lässt sich aber auch ohne Tk einsetzen.
Suchpfad
Gibt man ein Kommando ein, sucht die Shell die in der Umgebungsvariablen PATH gespeicherten Verzeichnisse der Reihe nach nach einer gleichnamigen ausführbaren Datei ab. Die erste Fundstelle wird benutzt; wird die Shell nicht fündig, so gibt sie die Fehlermeldung command not found aus, selbst wenn das Kommando irgendwo anders im Dateisystem existiert.
Entities
Hier: HTML-Kodierung für Zeichen, die im ASCII-Zeichensatz nicht auftauchen. Eine Entität beginnt in HTML immer mit einem kaufmännischen Und (&) und endet auf ein Semikolon, z. B. steht ü für ü, < für < und > für >.
Infos
[1] "Völlig link", "Answer Girl" im Linux-Magazin 10/1999: http://www.linux-magazin.de/ausgabe/1999/10/Answergirl/answergirl4.html
[2] Bjellis Perlwelt: http://perlwelt.horus.at/
[3] Viele Webseiten ändern: http://perlwelt.horus.at/beginning/MachSieAlle/home.html



