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