Programmieren

Link-Validator in Perl

Link-Sammlungen sind eine feine Sache. Sie eröffnen dem WWW-Nutzer schnellen Zugriff auf ausgewählte Internet-Angebote.

Link-Sammlungen sind eine schlimme Sache. Ehe man es sich versieht, sind WWW-Seiten verschwunden, weisen Querverweise in die Leere.

Man sieht: Es gibt einen erheblichen Unterschied zwischen einer Link-Sammlung und einer guten Link-Sammlung. Letztere benötigt erheblichen Pflegeaufwand, mindestens einmal pro Monat, besser noch: pro Woche, sollte der "Link-Provider" (wie man auf neudeutsch inzwischen diejenigen Zeitgenossen nennt, die auf ihren WWW-Seiten Querverweise anbieten) seine Link-Sammlung auf Validität überprüfen und gegebenenfalls nicht mehr auf das vermeintliche Ziel weisende Links ("broken links") korrigieren oder gar vollständig aus der Liste entfernen. Das ist übelste Handarbeit, zeitaufwendig und stupide. Also genau das richtige für eine Automatisierung durch ein kleines Perl-Skript.

Nun setzt der Zugriff auf WWW-Dokumente natürlich voraus, dass sich das Perl-Skript wie ein WWW-Client, ein sogenannter "User Agent", verhält. Wer "Netscape" kennt, dem dürfte die Implementierung von WWW-Client-Fähigkeiten nicht unbedingt als leichte Aufgabe erscheinen. Doch zum Glück standen schon andere vor dieser Herausforderung und haben sie überzeugend gelöst - und zwar in Form einer eigenen Perl-Ergänzung, der "LWP library", inzwischen wohl auch besser bekannt unter dem Namen "libwww-perl". Die neueste Version ist erhältlich unter

<http://www.linpro.no/lwp/>

Allerdings setzt die "libwww-perl", die zu bewältigende Aufgabe ist eben doch alles andere als trivial, diverse weitere Perl-Packages voraus:

URI
MIME-Base64
HTML-Parser
libnet
Digest::MD5

Diese können über das Comprehensive Perl Archive Network (CPAN) bezogen werden, entsprechende Links gibt es auf der oben ausgewiesenen "libwww-perl"-Homepage.

An dieser Stelle wird davon ausgegangen, dass die genannten Module bereits am richtigen Platz und einsatzbereit sind. Auf die Einzelheiten der durchaus unter Umständen nicht ganz unproblematischen Installation kann hier nicht näher eingangen werden.

LWP stellt dem Programmierer verschiedene Klassen und Module zur Verfügung. Normalerweise würde man für die Realisierung eines User Agents auf die gleichnamige Klasse

   LWP::UserAgent
  
zurückgreifen. Im Rahmen des vorliegenden Artikels soll jedoch stattendessen
   LWP::Simple
  
Verwendung finden. Damit stehen dem Programmierer einerseits nur grundlegende Funktionen zur Verfügung. Andererseits sind aber auch die Funktionsaufrufe und die Handhabung gegenüber der LWP::UserAgent-Klasse erheblich vereinfacht. Eingebunden werden die LWP::Simple-Funktionen über
   use LWP::Simple;
  
Ein WWW-Dokument kann nun mittels
   $content = get ($url);
  
aufgerufen werden, wobei in der Variable $url die URL des gewünschten Dokumentes gespeichert sein muss. Das gesamte (!) zurückgelieferte Dokument befindet sich daraufhin in $content. Findet der kontaktierte WWW-Server unter der übermittelten URL kein Dokument - und das ist für den hier verfolgten Zweck natürlich von besonderem Interesse -, so bleibt $content leer (undef). Mit diesem Wissen läßt sich bereits ein einfaches URL-Kontrollskript programmieren. Als "Kernstück" der Validitätsüberprüfung dient dabei eine simple If-Abfrage, mit der kontrolliert wird, ob der Variablen $content ein Inhalt zugewiesen wurde.
   if ($content) {
    print "Dieser Link ist valide.\n"
   } else {
    print "Dieser Link ist ungueltig.\n";
   }
  
Um das Skript flexibel zu halten, soll dabei die Variable $url mit dem ersten beim Aufruf übergebenen Parameter gefüllt werden. Zuvor erfolgt eine rudimentäre Eingabekontrolle, bei der lediglich überprüft wird, ob wirklich ein (und auch nur ein) Parameter übergeben wird (denkbar wäre an dieser Stelle sicherlich auch eine Routine, die den Parameter darauf überprüft, ob er den Anforderungen an eine URL (Protokoll://...) gerecht wird). Ist das nicht so, wird eine Fehlermeldung ausgegeben, anderenfalls wird mit der get-Anweisung das Dokument in die Variable $content eingelesen und die oben beschriebene If-Abfrage durchlaufen.

Zusammengesetzt sieht unser Skript "httpget.pl" somit wie folgt aus:

   #!/usr/bin/perl

   use LWP::Simple;

   $args = @ARGV;

   if ($args != 1) {

    print "Usage: perl httpget.pl URL\n";

   } else {

    $url = $ARGV[0];

    $content = get($url);

    if ($content) {
     print "Dieser Link ist valide.\n"
    } else {
     print "Dieser Link ist ungueltig.\n";
    }

   }
  
Der Aufruf erfolgt dabei gemäß der Syntax
   perl httpget.pl URL
  
Als Beispiel für eine valide Adresse kann man
   perl httpget.pl http://www.amigagadget.de
  
ausprobieren. Eine Fehlermeldung müßte hingegen erscheinen bei
   perl httpget.pl http://www.amigagadget.de/dieseurlexistiertnicht/
  
Nun ist dieses "httpget"-Skript zwar eine nette Spielerei, dem Ziel eines Link-Validators, der die Querverweise einer ganzen Homepage überprüft, sind wird damit jedoch nicht wesentlich näher gekommen. Hinzu kommen muss hier noch etwas anderes - eine Routine, die alle Hyperlinks in einem Dokument aufspürt. Dabei soll es mit dem Auffinden kompletter URLs sein Bewenden haben, relative Verweise, etwa im Stile von
   <a href="../index.html">
  
bleiben hier der Einfachheit halber außen vor. (Bei Bedarf kann diese Funktion aber gerne zum Bestandteil eines späteren "Gadget"-Beitrages werden.) Das ist im Rahmen der oben geschilderten Problemstellung auch sachgerecht, kann man doch davon ausgehen, dass der Link-Provider über das eigene Angebot den Überblick behält und sich nicht selbst durch das Löschen von Dateien diverse "broken links" erzeugt. (Soviel zum humoristischen Teil dieses Artikels. Zurück zur Realität.)

Die Aufgaben des erstrebten Skriptes, es sei "httpcheck" genannt, können grob in vier Gruppen unterteilt werden:

  1. Einlesen des zu überprüfenden Dokumentes
  2. Herausfiltern der Hyperlinks
  3. Überprüfung der Hyperlinks
  4. Ausgabe der Ergebnisse

Wenn es an die Details geht, muss das Skript somit zunächst einmal die URL des zu überprüfenden Dokumentes erhalten - hier kann auf die oben entwickelten Routinen zurückgegriffen werden. Dieses zu überprüfende Dokument wird nun komplett in einer Variable $checkcontent gespeichert. Des weiteren werden an dieser Stelle einige weitere Variablen initialisiert, als da wären

@badurls
Dieses Array enthält am Ende die vermutlich fehlerhaften Links.

$urlsnumber
In dieser Variable wird die Anzahl der gefundenen Links gespeichert.

$urlscounter
Damit der Anwender während des Überprüfungsvorganges, bei dem ja u.U. große Datenmengen übertragen werden, darüber im Bilde bleibt, welcher Hyperlink gerade untersucht wird, wird mit dieser simplen Counter-Variable mitgezählt.

Besondere Beachtung verdient dabei die Wertzuweisung an Variable $urlsnumber, da hier praktisch schon der zweite Programmschritt, das Herausfiltern der Hyperlinks, vorweggenommen wird. Benötigt wird dazu eine Funktion, mit der in einem Dokument alle Zeichenketten aufgespürt werden können, die mit

   <a href="http://
  

beginnen und mit

   "
  
enden. (Nicht mit ">, da ja die <a>-Tags auch weitere Parameter wie z.B. "target", etc. enthalten können. Eigentlich ist es schon nicht zwingend, dass der Tag mit <a href=" beginnt, für den hier verfolgten Demonstrationszweck sei diese Syntax aber mit der ganz herrschenden Praxis unterstellt.) Für solche Zwecke erscheinen die sogenannten "regulären Ausdrücke" Perls mit ihren Metazeichen geradezu prädestiniert. Relativ einfach ist noch die Umwandlung des Anfangs
   <a href="http://
  
in
   <a href="http:\/\/
  
Lediglich die Schrägstriche ("Slashes") müssen, da sie sonst als Metazeichen interpretiert werden, durch vorangestellte "Backslashes" gekennzeichnet werden. Auch das Ende des zu bildenden regulären Ausdrucks ist klar:
   "
  
Doch was kommt dazwischen ? Prinzipiell darf hier ja alles stehen. Beliebige Zeichen werden in Perl aber durch einen "." symbolisiert. Da nicht nur beliebige Zeichen, sondern auch beliebig viele davon vorkommen dürfen, muss man dieses Metazeichen durch das Metazeichen für das Suchen nach kein-, ein- oder mehrmaliger Wiederholung des vorangestellten Zeichens ergänzen, den Unix-"Joker" "*". Damit sähe der reguläre Ausdruck wie folgt aus:
   <a href="http:\/\/.*"
  
Doch damit sind wir noch nicht am Ziel. Zu beachten ist nämlich, dass Perl standardmäßig "gierig" sucht, d.h. gemäß den Vorgaben des regulären Ausdrucks nach möglichst langen Zeichenketten Ausschau hält. Wenn sich also etwa zwei Hyperlinks in einer Zeile (nicht durch "\n" getrennt) befinden, liefert der oben formulierte reguläre Ausdruck eine Zeichenkette zurück, die beim Anfang des ersten Hyperlinks beginnt und am schließenden Fragezeichen des zweiten endet. Das ist ein unerwünschtes Suchergebnis. In Kenntnis dieser Problematik wurde mit Perl 5 eine Metazeichen-Variante eingeführt, die dieses "gierige" Suchen unterbindet und dazu führt, dass der reguläre Ausdruck eine möglichst kurze Zeichenfolge zurückliefert. Bei dieser Metazeichen-Variante handelt es schlichtweg um ein angefügtes Fragezeichen. Somit sieht der korrekte reguläre Ausdruck so aus:
   <a href="http:\/\/.*?"
  
Bei der Anwendung dieses Ausdrucks auf das zu überprüfende Dokument ist des weiteren zu beachten, dass die Vergleichsoperationen unabhängig von Groß- oder Kleinschreibung sein sollten, was durch die Option "i" (case "i"nsensitve) erreicht wird. Schließlich ist es ja egal, ob der Tag mit
   <A HREF
  
oder mit
   <a href
  
oder einer Mischung aus beiden beginnt. Weiterhin ist es am effektivsten, die Vergleichsoperation gleich in einem Schritt global durchzuführen (Option "g") und die gefundenen Zeichenketten als eigenes Array zurückzugeben. Die in dem Dokument enthaltenen Hyperlinks lassen sich also mittels der Anweisung
   @urls = ($checkcontent =~ /<a href="http:\/\/.*?"/gi);
  
herausfiltern und im Array @urls speichern. Doch eigentlich ist das gar nicht nötig, da die einzelnen Feldelemente ja ohnehin nur als Parameter für den Aufruf einer Funktion benötigt werden, die diese URLs auf ihre Validität überprüft. Für eine solche Überprüfungsfunktion kann auf den schon in "gethttp" eingesetzten Vergleich des Resultats eines get-Aufrufs mit undef zurückgegriffen werden. Dabei müssen, da die Funktion ja diesmal aus Gründen der Programmästhetik und der Modularität als eigenständige Unterfunktion ausgegliedert werden soll, lediglich noch zwei lokale Variablen definiert werden, wobei der zweiten ($url) der Parameter zugewiesen wird, der der Unterfunktion übergeben wird:
   sub checklink {
    my $content;
    my $url = $_[0];
    $content = get($url);
    if ($content) {
    # Diese "Leerschleife" wird in der Endversion gefuellt.
    } else {
     push (@badurls,$url);
    }
   }
  
Die Verbindung zwischen dem Aufruf dieser Unterfunktion und dem Feld, in dem die herausgefilterten Hyperlinks gespeichert sind, schafft man nun mit Hilfe der Kontrollanweisung foreach. Sie definiert einen Programmblock, der für jedes Element eines Feldes einmal abgearbeitet wird, wobei das Element einer Schleifenvariablen, die hier schlicht $x heißen soll, zugewiesen wird.
   foreach $x ($checkcontent =~ /<a href="http:\/\/.*?"/gi) {
   }
  
Innerhalb der Schleife muss aber aus der Hyperlinkangabe, die ja noch das vorangestellte <a href=" sowie das schließende Anführungszeichen enthält, die "nackte" URL extrahiert werden. Dazu sucht man innerhalb der Schleifenvariable einfach nach den genannten Pattern und ersetzt sie durch eine leere Zeichenkette, entfernt sie also schlichtweg. Damit sieht das Herzstück unseres Link-Validator wie folgt aus:
   foreach $x ($checkcontent =~ /<a href="http:\/\/.*?"/gi) {
    $x =~ s/<a href="//i;
    $x =~ s/"//i;
    &checklink ($x);
   }
  
Schlußendlich gibt man noch die in @badurls gespeicherte Liste der vermutlich fehlerhaften Links aus - und fertig ist ein primitiver Link-Validator ! Zusammengesetzt (und ergänzt um diverse zusätzliche Programmausgaben) ergibt sich mithin folgendes Perl-Skript:
   #!/usr/bin/perl

   use LWP::Simple;

   $args = @ARGV;

   if ($args != 1) {

    print "Usage: perl httpcheck.pl URL\n";

   } else {

    $checkurl = $ARGV[0];

    $checkcontent = get($checkurl);

    @badurls =();

    if ($checkcontent) {
     $urlsnumber = (@urls = ($checkcontent =~ /<a href="http:\/\/.*?"/gi));
     $urlscounter = 0;

     foreach $x ($checkcontent =~ /<a href="http:\/\/.*?"/gi) {
      $x =~ s/<a href="//i;
      $x =~ s/"//i;
      &checklink ($x);
     }
     $badurlsnumber = @badurls;

     if ($badurlsnumber == 0) {

      print "All links are fine!\n";

     } else {

      print "List of probably broken links:\n";

      for ($i=0; $i<$badurlsnumber; $i++) {
       print "  $badurls[$i]\n";
     }

    } else {
     print "httpcheck: document not found!\n";
    }

   }
   sub checklink {

    my $content;
    my $url = $_[0];

    print "Checking Link " . ($urlscounter+1) . " of $urlsnumber...\n";
    $content = get($url);
    $urlscounter++;
    print "$url is ";

    if ($content) {
     print "valid.\n\n";
    } else {
     push (@badurls,$url);
     print "probably broken.\n\n";
    }
   }
  
Zu beachten ist natürlich, dass eventuell der Perl-Pfad in der ersten Zeile angepaßt werden muss. Der Aufruf des Skriptes erfolgt dann in der Form
   perl httpcheck.pl URL
  
Je nach Art und Umfang des über die URL identifizierten Dokumentes kann der Überprüfungsvorgang durchaus einige Zeit dauern.

Zudem läßt sich der Validator natürlich noch beliebig verbessern. U.a. wäre an die Verwendung von LWP::UserAgent, die Kontrolle auch relativer Links, den Einschluß anderer Übertragungsprotokolle (FTP, gopher) oder eine Ausgabe in Form eines HTML-Dokumentes zu denken. Und selbstverständlich gibt es das alles schon in unzähligen Spielarten im Internet. Zum spielerischen Kennenlernen der WWW-Anbindung Perls ist das Thema jedoch allemal geeignet.

(c) 1999 by Andreas Neumann

Zurück