Programmieren

Link-Validator in Perl (2)

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

   $content = get ($url);
  

durch

   $content = head ($url);
  

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:

   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";
    }
   }
  

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:

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

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

      LWP::UserAgent
  

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

      $ua = new LWP::UserAgent;
  

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:

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

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

      $response = $ua->request($request);
  

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

      $response->is_success
  

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

      $response->message
  

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

   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";
    }
   }
  

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

      chmod a+x httpcheck.cgi
  

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 ind 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

             Content-Type: text/html
           
  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:

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

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:

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

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

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

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:

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

Zurück