Liest der Bundeskanzler jeden Morgen alle Zeitungen? Nein, der feine Herr nutzt einen Clipping-Dienst. Das ist ein Stab von Leuten, die sich jeden Tag durch sämtliche bekannten Blätter wühlen, wichtige Artikel ausschneiden und eine Mappe zusammenstellen, mit der Creme de la Creme des Pressewesens, sozusagen.
Mein Arbeitspensum freilich läßt das des Kanzlers wie einen Hawaii-Aufenthalt erscheinen! Leider darf ich auch nicht soviel Geld verprassen, daß ich andere Leute zum Zeitunglesen anstellen könnte. Darum habe ich mir kurzerhand ein kleines Skript zusammengestellt, das die wichtigsten Neuigkeiten des Tages vom Internet pumpt: Den aktuellen Dilbert-Comic, die Schlagzeilen des Bayrischen Rundfunks und die aktuelle Erdbebenkarte der San-Francisco-Gegend, damit ich weiß, ob's letzte Nacht wirklich gerumpelt hat oder ob's doch nur wieder ein Bier zuviel war -- kommt leider vor!
Die Unix-Kiste in der Arbeit, die eh die ganze Nacht läuft, ruft
morgens um sieben das Skript clip.pl auf, das alles zusammensucht und
Texte und Bilder auf eine einzige Webpage packt. So kann
ich alle Daten auf einen Schlag einsehen, ohne unnötig Zeit mit Klicken und
Warten zu verplempern.
CGI.pm ist Dein FreundDas Skript aus Listing clip.pl nutzt für die HTML-Ausgaben das
schon ausführlich vorgestellte Modul CGI.pm von Lincoln D. Stein.
Damit CGI.pm, falls das Skript vom cron ohne Parameter
von der Kommandozeile aus aufgerufen wird, nicht endlos auf
CGI-Parameter aus der
Standardeingabe wartet, bietet das Modul ab Version 2.38 den
Schalter -noDebug an. Der use-Befehl mit der angehängten
Tag-Liste in Zeile 3 exportiert
also diejenigen Funkionen aus CGI.pm, die Standard- und Tabellen-HTML-Tags
erzeugen, läßt aber gleichzeitig das Skript normal von der
Kommandozeile laufen.
Die Funktionen get und getstore aus dem Modul LWP::Simple
aus der Bibliothek libwww von Gisle Aas holen Webseiten
vom Netz. get liefert den Inhalt der betreffenden Webseite als String
zurück, während getstore ihn gleich in einer angegebenen Datei
auf der Festplatte ablegt. Zeile 4 importiert getstore, get,
sowie das Fehlermakro RC_OK aus LWP::Simple.
Die kompakten LWP::Simple-Funktionen
kommen immer dann zum Einsatz, wenn keinerlei Redirects oder
Authorisierungsmaßnahmen den URL-Zugriff erschweren - falls doch,
müsste der LWP::UserAgent aus derselben Programmsammlung 'ran.
Die Zeilen 19 und 20 schreiben
mit den praktischen Funktionen aus CGI.pm den HTML-Start-Tag
und den Anfang einer Glossar-Liste, die später im Format
<DL>
<DT>Quelle <DD>Inhalt
<DT>Quelle <DD>Inhalt
...
</DL>
in der in Zeile 16 geöffneten Ausgabedatei clip.html stehen wird.
Der chdir-Befehl aus Zeile 14 versetzt das Skript in das Verzeichnis,
in dem später alle Ausgabedateien liegen sollen und sorgt
so dafür, daß das Skript immer dorthin schreibt, auch
wenn Dateien ohne Pfad angegeben werden. Mit den Konfigurationsparametern
aus den Zeilen 9 und 10 läßt sich dieses Verzeichnis sowie der
Name der Ergebnisdatei an die lokalen Erfordernisse anpassen.
Der Bayerische Rundfunk aktualisiert stündlich eine Webpage, die
mit etwa fünf Schlagzeilen einschließlich kleiner Dreizeiler
genau das Maß an Politik bietet, das ich noch vertragen kann,
ohne mich zu langweilen. Da nicht die gesamte Seite einschließlich
aller Logos und Werbung interessiert, schneidet clip.pl den HTML-Text
zwischen den Tags <A NAME="1" und </TT> aus -- irgendwann
einmal habe ich herausgefunden, daß zwischen diesen Tags die
Schlagzeilen stehen. Die Funktion grab_and_grep, die ab Zeile
63 definiert ist, nimmt als Argumente einen URL und einen regulären
Ausdruck entgegen. Nachdem grab_and_grep die Seite mit der
get-Funktion
(LWP::Simple) geholt hat, prüft sie, ob deren Inhalt irgendwo
auf den als String hereingereichten regulären Ausdruck paßt.
Die eval-Anweisung aus Zeile 75 konstruiert zur Laufzeit die Befehlsfolge
$doc =~ /<A NAME="1".*<\/TT>/s; return $&
und führt sie aus.
Der reguläre Ausdruck strebt im Dokument $doc eine maximale
Abdeckung (.*) zwischen den Tags <A NAME="1" und </TT>
an,
wegen des /s-Modifizierers schluckt .* zeilenübergreifend Zeichen.
Das Teildokument, auf das der reguläre Ausdruck paßt, liegt anschließend
in der Spezial-Variablen $&, die die folgende return-Anweisung an
das Hauptprogramm zurückgibt.
Dieses nutzt die Funktionen dt und dd aus CGI.pm, um die extrahierte
Information schön als Glossarliste strukturiert in der Ergebnisdatei abzulegen.
Die HTML-Tags, auf die der Code anspringt, können sich natürlich
kurzfristing ändern. Falls der Bayrische Rundfunk das Format der Webseite
ändert, muß clip.pl entsprechend nachgezogen werden. Entsprechendes
gilt für die nachfolgenden Funktionen: auch URLs können sich kurzfristig
ändern.
Graphisch aufbereitete Daten über die
letzten Erbeben, die sich in der Bay-Area ereigneten, liegen als GIF-Bild
auf einem Server der US-Regierung. Diese Datei vom Netz zu ziehen und lokal
abzuspeichern, ist mit LWP::Simple und getstore ein Kinderspiel.
Entspricht der Rückgabewert dem Wert des Fehlermakros RC_OK, ging
alles gut. RC_OK ist nicht
etwa ein Skalar, sondern eine von LWP::Simple exportierte Funktion.
Im Gutfall fehlt nur noch, einen IMG-Link in unsere Clipping-Datei
zu schreiben, der auf die Quake-Datei zeigt -- fertig ist der Lack!
United Media veröffentlicht täglich einen neuen Dilbert-Comic, den
Leute wie ich, die in einem Großraumbüro mit Stellwand-Quadraten
(``Cubicles'') arbeiten, natürlich unbedingt lesen müssen. Leider
verunzieren die Komiker dort die Seite mit Werbung, und damit's nicht
ganz so einfach ist, nur den Strip zu extrahieren, hängen sie an
den Image-Namen das Datum und die (unvorhersagbare) Uhrzeit dran, z. B.
dilbert980118104253.gif.
Da muß schweres Gerät ran, die Funktion dilbert_to_file zeigt
ab Zeile 79, wie es geht: Die get-Funktion
holt die Seite, die unter anderem irgendwo den Image-Link enthält,
um sie anschließend mit dem HTML::Treebuilder zu parsen. Das
Parse-Objekt verfügt über die Methode extract_links, die
das SRC-Attribut aller Tags vom Format <IMG SRC=...>
herausfiltert, falls, wie im Listing, die ihr übergebene Liste das
Element img enthält. extract_links gibt dabei eine Referenz
auf einen Array zurück, der als Elemente für jeden gefundenen
SRC-Attributwert eine Referenz auf einen weiteren Array enthält,
welcher wiederum als erstes Element den URL des Bildes führt, der
wiederum ... kleiner Scherz, der URL ist, was wir brauchen :).
Dieser URL ist im Falle der
United-Media-Seite relativ, also auf den URL der Seite bezogen. Um
das Bild vom Netz zu holen, muß er absolut, also als sauberer
http://...-URL vorliegen. Für die Umwandlung bietet sich die abs-Methode
des URI::URL-Pakets an, also wird flugs ein neues URI::URL-Objekt
mit dem relativen Link erzeugt und die abs-Methode mit dem Basis-URL
der United-Media-Seite ($url) aufgerufen, worauf $abslink in
Zeile 94 den absoluten URL enthält.
Da auf der Seite natürlich mehrere Links zu finden sind, extrahiert die
for-Schleife einen nach dem anderen, bis einer daherkommt, der wie
das Dilbert-Bild aussieht: /dilbert\d+\.gif$/ ist der passende reguläre
Ausdruck, der auf Strings im Format ___dilbert980118104253.gif wartet.
Was bleibt, ist nur, den Parse-Baum zu löschen, das Bild mit
getstore vom Netz zu holen und auf der Platte zu speichern.
Nun muß noch ein Eintrag in die Tabelle des cron, damit dieser
das Skript jeden Tag ausführt, sagen wir um sieben in der Frühe:
00 7 * * * /home/mschilli/bin/clip.pl
Dann enthält clip.html im eingestellten Verzeichnis kurze
Zeit später alle gewünschten Informationen und auch die
Zusatz-Dateien quake.gif und dilbert.gif liegen im gleichen Verzeichnis
vor. Kommt der schlaftrunkene Anwender um neun ins Büro, zeigt der Browser,
falls er ihn mit File -> Open File
auf clip.html einstellt, eine schöne Clipping-Mappe an. Schon wieder
kein Erdbeben? Muß wohl doch das Bier gewesen sein gestern nacht ...
clip.pl
001 #!/usr/bin/perl -w
002 ####################################################
003 # Michael Schilli, 1998 (mschilli@perlmeister.com)
004 ####################################################
005
006 ###################### Module ######################
007 use CGI qw/:standard :html3 -noDebug/;
008 use LWP::Simple qw/get getstore RC_OK/;
009 use HTML::TreeBuilder; # HTML-Parser
010
011
012 ################## Konfiguration ###################
013 $clipdir = "/home/mschilli/clip";
014 $htmlfile = "clip.html";
015
016
017 ################### Datei öffnen ###################
018 chdir($clipdir) || die "Cannot chdir to $clipdir";
019
020 open(OUT, ">$htmlfile") ||
021 die "Cannot open $htmlfile";
022 # Titel
023 print OUT start_html('-title' => "Clipping-Dienst");
024 print OUT dl; # Listenanfang
025
026
027 ##### Kurznachrichten des Bayrischen Rundfunks #####
028 $ret = grab_and_grep(
029 "http://www.br-online.de/news/aktuell",
030 "/<A NAME=\"1\".*<\\/TT>/s");
031
032 print OUT dt(h1("Bayrischer Rundfunk")), dd($ret);
033
034
035 ############ Erbeben-Karte der Bay-Area ############
036 $url = "http://quake.wr.usgs.gov/recenteqs" .
037 "/Maps/SF_Bay.gif";
038 $localfile = "quake.gif";
039
040 print OUT dt(h1("Aktuelle Erbeben-Karte"));
041
042 if(getstore($url, $localfile) == RC_OK) {
043 print OUT dd(img({src => "quake.gif"}));
044 } else {
045 print OUT dd("Cannot get $url");
046 }
047
048
049 ############### Dilbert Comic-Strip ################
050 $url = "http://www.unitedmedia.com/comics/dilbert/";
051 $localfile = "dilbert.gif";
052
053 print OUT dt(h1("Dilbert"));
054
055 if(dilbert_to_file($url, $localfile) == RC_OK) {
056 print OUT dd(img({src => "dilbert.gif"}));
057 } else {
058 print OUT dd("Cannot get $url");
059 }
060
061 ##################### Abschluß #####################
062 print OUT end_html;
063 close(OUT);
064
065
066 ####################################################
067 sub grab_and_grep {
068 ####################################################
069 # Webseite holen und Text nach angegebenen
070 # Muster extrahieren
071 ####################################################
072 my ($url, $regex) = @_;
073
074 my $doc;
075
076 ($doc = LWP::Simple::get $url) ||
077 return "Cannot get $url";
078
079 eval "\$doc =~ $regex; return \$&";
080 }
081
082 ####################################################
083 sub dilbert_to_file {
084 ####################################################
085 # Dilbert-Seite ($url) holen, Comic-URL extra-
086 # hieren, GIF holen und lokal in $file speichern
087 ####################################################
088 my ($url, $file) = @_;
089
090 my ($doc, $abslink, $link);
091
092 ($doc = get $url) || return 0;
093
094 my $tree = HTML::TreeBuilder->new->parse($doc);
095
096 for (@{$tree->extract_links(qw/img/)}) {
097 $link = URI::URL->new($_->[0]);
098 $abslink = $link->abs($url);
099 last if $abslink =~ /dilbert\d+\.gif$/;
100 }
101
102 $tree->delete(); # Parse-Baum löschen
103
104 getstore($abslink, $file); # Lokal speichern
105 }
![]() |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |