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

Der eine oder andere Leser wird sich vielleicht noch an die Entwicklung
eines primitiven Link-Validators in Perl erinnern, die im Rahmen der
"Programmieren"-Rubrik der "AmigaGadget"-Ausgabe 40 geschildert wurde.
In der einzigen Reaktion, die auf diesen Beitrag erfolgte, wurde (zu
Recht) kritisiert, dass sich das Perl-Skript stets das gesamte HTML-Dokument
übertragen läßt, nur um zu überprüfen, ob die URL noch valide ist. Aus
diesem Grund soll Hauptziel der hiermit vorliegenden Fortsetzung die
Berücksichtigung dieses Punktes sein. Darüber hinaus wird im zweiten
Teil des Artikels das bisher nur zur Verwendung auf Kommandozeilenebene
konzipierte Skript selbst WWW-fähig gemacht.

Doch zunächst geht es darum, die bisher verwendete Methode, die Seite
immer komplett anzufordern, deren Existenz verifiziert werden soll, durch ein
übertragungskapazitätsparenderes Verfahren zu ersetzen.  Dies kann natürlich
auch mit der bislang eingesetzte LWP::Simple-Klasse noch erreicht werden,
indem man den Aufruf
#C10
    $content = get ($url);
#C21

durch

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

ersetzt. In diesem Fall enthält die Variable $content zwar nicht, wie ihr
Name andeutet, den Inhalt der HTML-Datei, sondern lediglich diverse
Header-Informationen, die der kontaktiere WWW-Server zurückliefert. Existiert
unter der verwendeten URL kein Dokument, so ist die Variable jedoch genauso
undefiniert wie bei einem get()-Aufruf. Damit kann man also schon einiges
machen. Zum Beispiel ließe sich die checklink-Unterroutine somit wie folgt
umschreiben:

#C10
 sub checklink {

  my $url = $_[0];

  print "Checking Link " . ($urlscounter+1) . " of $urlsnumber...\n";
  $urlscounter++;
  print "$url is ";
  if (head($url)) {
   print "valid.\n\n";
  } else {
   push (@badurls,$url);
   print "probably broken.\n\n";
  }
 }
#C21

Hier wurde sowohl die Variable $content eingespart als auch der get- durch
einen netzschonenderen head-Aufruf ersetzt. Eigentlich ist damit dem
eingangs aufgestellten Erfordernis schon Genüge getan, doch damit soll es
natürlich hier nicht sein Bewenden haben. Zunächst soll - allerdings nur
als kleiner Exkurs - ein Blick auf den Inhalt der übermittelten
Header-Informationen geworfen werden. LWP::Simple gibt diese in Form einer
aus fünf Elementen bestehenden Liste zurück, so dass ein Funktionsaufruf, bei
dem mit den ermittelten Werten später gearbeitet werden sollte, so aussähe:

#C10
    ($content_type, $document_length,
     $modified_time, $expires, $server) = head ($url);
#C21

Die Variablennamen dürften sich selbst erklären.

Richtig mächtig wird die "LWP Library" jedoch erst, wenn die im ersten Teil
bereits kurz erwähnte Klasse

#C10
    LWP::UserAgent
#C21

zum Einsatz kommt. Natürlich hat die erweiterte Funktionalität ihren Preis,
die einfachen head-, bzw. get-Aufrufe sind mit diesem Modul nicht mehr
möglich. (Was aber nicht weiter schlimm ist, da ja beiden Klassen kumulativ
eingebunden werden können.) Statt dessen muss man zunächst einen sogenannten
User-Agent definieren, was etwa mit

#C10
    $ua = new LWP::UserAgent;
#C21

geschieht. Nun muss dieser User-Agent dazu gebracht werden, einen HTTP-Request,
also eine Anfrage an einen WWW-Server, zu senden. Dazu muss diese zunächst
einmal festgelegt werden. Hierfür ist ein Konstruktor-Aufruf für ein
HTTP::Request-Objekt erforderlich, dem als Variablen der auszusendende
HTTP-Befehl und die zugehörige URL übergeben werden müssen. Da für den
definierten Zweck auch hier nur die Header-Informationen benötigt werden,
müßte angesichts der Tatsache, dass der dafür geeignete HTML-Befehl "HEAD"
heißt, eine solche Definition wie folgt aussehen:

#C10
    $request = new HTTP::Request ('HEAD', $url);
#C21

Damit diese Anfrage auch ausgesendet wird, muss sie noch dem User-Agent als
"Request" übergeben werden:

#C10
    $response = $ua->request($request);
#C21

Die Variable $response enthält nun einen Hash, über den weitere sehr
nützliche Informationen abgerufen werden können. Im vorliegenden Kontext
interessiert dabei lediglich, ob der HTTP-Request ins Leere ging, und
gegebenenfalls, warum das passierte. Erstgenannte Information erhält man
über die boolsche Variable

#C10
    $response->is_success
#C21

und für die sich eventuell stellende Frage nach der Fehlerursache erhält man
sogar eine vollständige Fehlermeldung über

#C10
    $response->message
#C21

Damit steht die vollständig überarbeitete checklink-Unterroutine:

#C10
 sub checklink {
  my $request;
  my $url = $_[0];

  print "Checking Link " . ($urlscounter+1) . " of $urlsnumber...\n";

  $urlscounter++;
  print "$url is ";
  $request = new HTTP::Request ('HEAD', $url);
  my $response = $ua->request($request);

  if ($response->is_success) {
   print "valid.\n\n";
  } else {
    push (@badurls,$url);
    print "probably broken: ";
    print $response->message . "\n\n";
  }
 }
#C21

Damit ist der Link-Validator bereits um wichtige Fähigkeiten erweitert worden.
Dass das noch nicht das Ende aller Programmier-Weisheit darstellt, soll nicht
verschwiegen werden. Insbesondere wäre eine sorgfältige Analyse einer
eventuell gescheiterten HTTP-Anfrage geboten. Doch im Rahmen dieses kleinen
Programmiertips soll das Erreichte insoweit erst einmal genügen.

Statt dessen ist nun eine weitere Erweiterung ins Auge zu fassen - httpcheck.pl
soll selbst über einen WWW-Browser gestartet werden können. Dazu muss es
dem sogennanten CGI-Kommunikationsmodell entsprechen. Damit ein Programm mit
einem WWW-Server kommunizieren kann, ist die Einhaltung gewisser
Vermittlungsstandards erforderlich. Man spricht dabei von dem "Common Gateway
Interface", kurz "CGI". Voraussetzung für das ganze ist in praktischer Hinsicht
natürlich, dass man Zugang zu einem WWW-Server hat, der nicht nur die
in "Gadget"#40 genannten Perl-Module (und Perl selbst) zur Verfügung stellt,
sondern der darüber hinaus auch die Verwendung eigener CGI-Skripte erlaubt.
Sollte diese Möglichkeit prinzipiell bestehen, müssen dabei jedoch gewisse
Anforderungen erfüllt werden (etwa dergestalt, dass die CGI-Skripten in einem
eigenen Unterverzeichnis "cgi-bin" zu plazieren sind), so ist im folgenden
davon auszugehen, dass diesen entsprochen wird. Wichtig ist natürlich auch,
dass dem Skript vor dem HTTP-Aufruf die richtigen Rechte zugewiesen werden,
wozu in der Regel ein

#C10
    chmod a+x httpcheck.cgi
#C21

ausreichen dürfte.

Programmiertechnisch stellt die Herbeiführung einer rudimentären CGI-Fähigkeit
des Skriptes - und um nur eine solche kann es in diesem Rahmen gehen ! - kein
großes Problem dar. Zu beachten sind eigentlich lediglich drei Punkte:

    1. Es muss festgestellt werden, ob eine Ausgabe im HTML-Format
       gewünscht wird oder nicht. Alle nachfolgenden Anforderungen
       sind nur zu berücksichtigen, wenn es um eine solche Ausgabe geht.

    2. Der korrekte MIME-Header muss übergeben werden. Für eine HTML-Datei
       ist dies die von zwei (!) Carriage Returns ("\n") gefolgte Zeichenfolge

#C10
       Content-Type: text/html
#C21

    3. Die Ausgabe ist, wo erforderlich, um HTML-Spezifika zu ergänzen. Das
       bedeutet vor allem, dass der korrekte Rahmen (<html>- und
       <body>-Container) geschaffen und für Zeilenumbrüche gesorgt wird.

Zur Realisierung von Punkt 1 bietet sich eine "Hack"-Lösung an, die den hier
gestellten Anforderungen aber allemal genügt. Legt man fest, dass bei einem
HTTP-Aufruf des Skriptes nicht nur die zu überprüfende URL, sondern in einer
zweiten Variable auch die Zeichenkette "html" zu übergeben ist, so braucht
man innerhalb des Skriptes nur die Existenz dieser Variable zu überprüfen.
Natürlich ist das alles andere als elegant und verhindert weder, dass über
die Kommandozeilenebene eine HTML-Ausgabe herbeigeführt wird, noch, dass
via HTTP ein Aufruf ohne diese zweite Variable erfolgt, das Skript damit
nicht korrekt in einer CGI-Umgebung läuft und statt dessen eine Fehlermeldung
produziert - bei Bedarf können all diese Schwachstellen in zukünftigen
Beiträgen an dieser Stelle überwunden werden. Diesmal soll es jedoch mit dem
ergänzenden Hinweis sein Bewenden haben, dass der korrekte Skript-Aufruf über
einen Browser somit folgendermaßen aussehen muss:

#C10
    http://[server][pfad]httpcheck.cgi?[url]+html
#C21

Läge das httpcheck-Skript also beispielsweise im Verzeichnis "cgi-bin" auf
dem Server "www.amigagadget.de" (was *nicht* der Fall ist !) und sollen die
externen Links auf der WWW-Seite mit der URL "http://www.amiga.de" überprüft
werden, sähe der Aufruf wie folgt aus:

#C10
    http://www.amigagadget.de/cgi-bin/httpcheck.cgi?http://www.amiga.de+html
#C21

Im der nun folgenden - vorläufigen - Endversion des verbesserten
Link-Validators befinden sich gelegentlich Programmzeilen im Stile von

#C10
    ($ARGV[1] eq "html") ? print "<p>" : 1 ;
#C21

Hier kommt eine der Besonderheiten von Perl zum Einsatz, die die Unlesbarkeit
dieser Sprache sicherstellen. Der Perl-Ausdruck `($ARGV[1] eq "html")' wird als
Kontrollstruktur verwendet. Ist er wahr (TRUE), so wird der Perl-Ausdruck
hinter dem Fragezeichen ausgeführt, andernfalls gelangt der Ausdruck hinter
dem Doppelpunkt zur Ausführung. Da hier irgendein valider Perl-Ausdruck
erforderlich ist, das Skript in diesem Fall jedoch nichts machen soll,
kann man sich mit der Angabe des "wahren" Ausdrucks "1" begnügen. Faßt man
all dies in einem Listing zusammen, so ergibt sich folgendes Skript:

#C10
 #!/usr/bin/perl

 use LWP::Simple;
 use LWP::UserAgent;

 $ua = new LWP::UserAgent;

 $args = @ARGV;

 if (($args != 1) && ($args != 2)) {

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

 } else {

  $checkurl = $ARGV[0];

  if ($args == 2) {
   if ($ARGV[1] eq "html") {
    print "Content-Type: text/html\n\n";
    print "<html><body><h1>httpcheck-Ausgabe:</h1>";
   }
  }

  $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;

   ($ARGV[1] eq "html") ? print "<p>" : 1 ;

   if ($badurlsnumber == 0) {

    print "All links are fine!\n";

   } else {


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

    for ($i=0; $i<$badurlsnumber; $i++) {
     ($ARGV[1] eq "html") ? print "<p>" : 1 ;
     print "  $badurls[$i]\n";
    }

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

  ($ARGV[1] eq "html") ? print "</body></html>" : 1 ;

 }

 sub checklink {
  my $request;
  my $url = $_[0];

  ($ARGV[1] eq "html") ? print "<p>" : 1 ;
  print "Checking Link " . ($urlscounter+1) . " of $urlsnumber...\n";
  ($ARGV[1] eq "html") ? print "<p>" : 1 ;
  $urlscounter++;
  print "$url is ";

  $request = new HTTP::Request ('HEAD', $url);
  my $response = $ua->request($request);

  if ($response->is_success) {
   print "valid.\n\n";
  } else {
    push (@badurls,$url);
    print "probably broken: ";
    print $response->message . "\n\n";
  }
 }

#Pinsel gadget42:pinsel/an rechts
