#Titel Programmieren / Link-Validator in Perl
#Logo gadget40:Pinsel/AG.Prog
#Font Losse 16
#C31
         Link-Validator in Perl
#Font topaz 8
#C21

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

#C10
<http://www.linpro.no/lwp/>
#C21

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

#C10
  URI
  MIME-Base64
  HTML-Parser
  libnet
  Digest::MD5
#C21
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

#C10
  LWP::UserAgent
#C21

zurückgreifen. Im Rahmen des vorliegenden Artikels soll jedoch stattendessen

#C10
  LWP::Simple
#C21

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

#C10
use LWP::Simple;
#C21

Ein WWW-Dokument kann nun mittels

#C10
$content = get ($url);
#C21

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.

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

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:
#C10
 #!/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";
  }

 }
#C21

Der Aufruf erfolgt dabei gemäß der Syntax

#C10
perl httpget.pl URL
#C21

Als Beispiel für eine valide Adresse kann man

#C10
perl httpget.pl http://www.amigagadget.de
#C21

ausprobieren. Eine Fehlermeldung müßte hingegen erscheinen bei

#C10
perl httpget.pl http://www.amigagadget.de/dieseurlexistiertnicht/
#C21

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

#C10
<a href="../index.html">
#C21

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

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

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

#C12
$urlscounter
#C21
 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

#C10
<a href="http://
#C21

beginnen und mit

#C10
"
#C21

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

#C10
<a href="http://
#C21

in

#C10
<a href="http:\/\/
#C21

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:

#C10
"
#C21

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:

#C10
<a href="http:\/\/.*"
#C21

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:

#C10
<a href="http:\/\/.*?"
#C21

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

#C10
<A HREF
#C21

oder mit

#C10
<a href
#C21

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

#C10
  @urls = ($checkcontent =~ /<a href="http:\/\/.*?"/gi);
#C21

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:

#C10
  sub checklink {
   my $content;
   my $url = $_[0];
   $content = get($url);
   if ($content) {
   # Diese "Leerschleife" wird in der Endversion gefuellt.
   } else {
    push (@badurls,$url);
   }
  }
#C21

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.

#C10
  foreach $x ($checkcontent =~ /<a href="http:\/\/.*?"/gi) {
  }
#C21

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:

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

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:
#C10

 #!/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";
  }
 }

#C21
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

#C10
perl httpcheck.pl URL
#C21

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.
#Pinsel gadget40:Pinsel/an rechts
