Wer wollte nicht schon einmal auf einer Bergwanderung seine Aktienkurse abfragen? Das heutige Skript kommuniziert per E-Mail mit einem 2-Way-Pager.
Eigentlich war ich ja gegenüber diesem ganzen drahtlosen Gesummse eher skeptisch eingestellt. Aber seit man mir von Firmenseite einen Pager angedreht hat, schleppe ich ihn auch brav mit mir herum. Das Teil kann E-Mails empfangen und senden -- und so dachte ich mir: Warum nicht das Angenehme mit dem Nützlichen verbinden?
Ein Aktienkursservice wär's doch: Der Pager schickt eine E-Mail mit dem Kürzel der gesuchten Aktie im Mailrumpf los. Der Aktienkursservice, der auf einem Server mit Internetanschluss als Perlskript läuft, fängt die Mail ab, extrahiert das Kürzel und schickt, genau wie ein Browser, einen Web-Request an die Yahoo-Finanzseite aus. Aus dem zurückkommenden HTML extrahiert das Skript anschließend den Aktienkurs als relevante Information -- und schickt sie an den Absender der E-Mail, den Pager, zurück. Na, da weiß selbst der Almödi, was an den Börsen der Welt abgeht, wenn der Pager heftig in der Hosentasche vibriert.
Abbildung 1: Der kleine 2-Way-Pager |
Sendet also der 2-Way-Pager etwa eine E-Mail mit dem Inhalt "aol"
an
die Adresse benutzer@irgendwo.com, sorgt dort eine .forward
-Datei
im Home-Verzeichnis des Benutzers benutzer
dafür, dass der
Sendmail-Dämon die Mail nicht
in die normale Eingangsschlange einreiht, sondern einem Skript nach
Listing getquote.pl
zu Fressen gibt. Dieses extrahiert das Kürzel der
gesuchten Aktie, kontaktiert de.finance.yahoo.com, tippt den
Namen in das Suchfeld in Abbildung 2,
stellt am Auswahlschalter den Börsenplatz
Frankfurt ein und simuliert schließlich einen gedrückten
``Kurse abfragen''-SubmitSubmit-Knopf.
Es liest die zurückkommende HTML-Seite und fieselt den gesuchten Kurs
der AOL-Aktie
aus einer der Tabellen, die Yahoo im Ergebnis nach Abbildung 3
unterbringt. Aus der ursprünglich empfangenen Email
geht entweder aus dem Reply-To:
oder From:
-Header
hervor, wer sie abschickte, und an genau diese Adresse schickt das
Skript anschließend die extrahierte Information ab.
Abbildung 2: Das Eingabeformular des Yahoo-Aktienservice |
Abbildung 3: Die Antwort des Yahoo-Aktienservice |
Das klingt nach verhältnismäßig viel Arbeit, geht aber wie geschmiert, weil einige schlaue Module vom CPAN die Hauptarbeit erledigen -- nur 69 Zeilen hat das gesamte Skript!
Die Zeilen 3 und 4 definieren zwei Parameter: $MAIL_INPUT
gibt
an, woher die ankommende Email stammt. Legt $MAIL_INPUT
den
Namen einer Datei fest, liest das Skript diese ein, was besonders zum
Testen des Skripts nützlich ist. Unter Produktionsbedingungen
steht $MAIL_INPUT
selbstverständlich auf "-"
, was den
open
-Befehl in Zeile 15 dazu veranlasst, auf der Standardeingabe
zu lauschen, wo der sendmail
-Dämon die E-Mail auch hinleiten wird.
Der andere Parameter, $MAIL_HOST
, gibt den Server des Internetproviders
für ausgehende Mails an, der später dazu verwendet wird, Antworten auf
Anfragen zurückzuschicken.
Zeile 6 weist das Skript an, streng zu sein und darauf zu achten, dass
nicht unabsichtlich globale Variablen, symbolische Referenzen oder
sonstige schmutzige Tricks einfließen, die Perl in seiner unendlichen
Güte sonst durchgehen lässt. Zeile 7 schaltet den Warnungsmodus ein,
der vor Perl 5.6 noch mit dem Kommandozeilenschalter -w
aktiviert
werden musste.
Die Zeilen 9 bis 12 ziehen die praktischen Module vom CPAN herein, die
die Arbeit erledigen: Mail::Internet
von Graham Barr
erleichtert das Analysieren und Schreiben von Email in Perl.
LWP::UserAgent
und HTTP::Request::Common
von
Gisle Aas wickeln den Web-Verkehr
ab und HTML::TableExtract
von Matt Sisk
ist ein neuer Parser zum komfortablen
Durchforsten von HTML-Texten nach Tabellen mit bestimmten Kriterien.
01 #!/usr/bin/perl 02 03 my $MAIL_INPUT = "-"; 04 my $MAIL_HOST = "mail.subbrhosting.net"; 05 06 use strict; 07 use warnings; 08 09 use Mail::Internet; 10 use LWP::UserAgent; 11 use HTTP::Request::Common; 12 use HTML::TableExtract; 13 14 # Mail einlesen 15 open MAILIN, "<$MAIL_INPUT" or 16 die "Cannot open $MAIL_INPUT"; 17 my @data = <MAILIN>; 18 close MAILIN; 19 20 # Mail analysieren 21 my $mail = Mail::Internet->new(\@data); 22 my $stock = $mail->body()->[0]; 23 chomp $stock; 24 25 # Antwort zusammenbasteln 26 my $reply = $mail->reply(); 27 my $quote = get_quote($stock); 28 $reply->body( [$quote] ); 29 30 # ... und absenden 31 $reply->smtpsend(Host => $MAIL_HOST) or 32 die "Reply mail failed"; 33 34 ################################################## 35 sub get_quote { 36 ################################################## 37 my $stock = shift; 38 39 my $URL = "http://de.finance.yahoo.com/q?m=F&" . 40 "s=$stock" . "&d=v1"; 41 42 my $result = ""; 43 44 my $ua = LWP::UserAgent->new(); 45 my $resp = $ua->request(GET $URL); 46 47 if($resp->is_error()) { 48 # Fehler beim Holen der Webseite? 49 my $error = sprintf "Error: %s", 50 $resp->message(); 51 return $error; 52 } 53 54 my $data = $resp->content(); 55 56 my $te = new HTML::TableExtract( depth => 0, 57 count => 4 ); 58 $te->parse($data); 59 60 # Alle Tabellenreihen zusammen darstellen 61 my @rows = $te->rows; 62 shift @rows; 63 foreach my $row (@rows) { 64 $result .= join(' ', $row->[0], $row->[4]); 65 $result .= " ** "; 66 } 67 68 return $result; 69 }
Die Zeilen 15 bis 18 lesen die ankommende E-Mail
auf einen Rutsch in den Array @data
ein, wobei jedes Element
einer Zeile der ursprünglichen Nachricht entspricht.
Zeile 21 kreiiert ein neues
Mail::Internet
-Objekt aus einer Referenz auf den Mailzeilen-Array.
Dieses Objekt wird uns die lästige Header-Setzerei und -Leserei ersparen,
da wir zukünftig nur noch mit Methoden auf das Mailobjekt zugreifen.
Zeile 22 liest die erste Zeile des Mailtexts. Die body
-Methode liefert
laut Mail::Internet
-Dokumentation eine Referenz auf einen Array, der
die Zeilen des Mailtexts als Elemente enthält. $mail->body()->[0]
liefert also genau die erste Zeile des Mailtexts -- wo wir den
vom Benutzer angegebenen Namen der gesuchten Aktie erwarten.
Zeile 23 schneidet noch den Zeilenumbruch ab, falls einer
vorhanden ist.
Zeile 26 erzeugt in $reply
eine Referenz auf ein neues
Mail::Internet
-Objekt für die Rückantwort.
Wie ein guter Mail-Client stellt Mail::Internet
hierzu ein
Re:
mit dem ursprünglichen Betreff in die Subject-Zeile und
besetzt den Body mit dem eingerückten und markierten Text
der vorausgegangenen Mail vor.
Der Adressat ist der Absender der ursprünglichen Nachricht.
Die reply
-Methode des Mail::Internet
-Pakets sucht zunächst nach
einem Reply-to
-Header in der ursprünglichen Mail und fällt auf
den From
-Header zurück, falls jener nicht vorhanden ist, um
den neuen Adressaten zu ermitteln.
Zeile 28 überschreibt mit
der body
-Methode den Nachrichtentext der ausgehenden Mail mit dem
Aktienkursergebnis, das
vorher die get_quote
-Methode mit dem Aktiennamen als Argument
vom Internet eingeholt hat. Die body()
-Methode in Zeile 28
nimmt eine Referenz auf einen Array entgegen, dessen Elemente die Zeilen
der Nachricht enthalten. Im Fall der Funktion get_quote()
, die nur
einen einzigen Skalar zurückliefert, enthält der frisch erzeugte
anonyme Array den Ergebnisstring als einziges Element.
Zeile 31 schließlich sendet die Email zurück.
Neben der smtpsend
-Methode mit dem Mailhost des Internetproviders
als Argument böte Mail::Internet
außerdem noch die
send
-Methode,
die sich auf den unter Unix herumlungernden mail
-Client stützt.
Aber das nur, falls man den Sendmail-Dämon nicht direkt ansprechen möchte.
Die Funktion get_quote
ab Zeile 35 nimmt das auf yahoo.de
übliche Aktienkürzel entgegen
(bmw
, aol
oder z.B. auch 575800
für die Höchst AG
)
und nutzt das Modul LWP::UserAgent
, um
einen Agenten zu erzeugen, der sich benimmt wie ein Browser und
die Yahoo-Seite aus Abbildung 2 befrägt.
Welche Formularparameter Yahoo erwartet, lässt sich einfach
anhand der HTML-Codes der Seite aus Abbildung 2 ableiten -- oder
auch gerne mit dem loggenden Proxy aus [1]. Es zeigt sich,
dass das Skript auf der Yahoo-Seite
http://de.finance.yahoo.com/q
heisst, und drei Query-Parameter
erwartet: Mit m=F
steht der Börsenplatz auf Frankfurt,
mit s=aol
wird das Aktienkürzel auf aol
gesetzt und
mit d=v1
kommt noch etwas Geheimnisvolles hinzu, das
offensichtlich die Darstellung beeinflusst.
Zeile 39 in getquote.pl
jedenfalls baut diesen URL zusammen,
Zeile 44 erzeugt einen neuen Useragenten und Zeile 45 feuert den Request
ab. Zeile 47 überprüft mit der is_error
-Methode, ob bei der
Übertragung ein Fehler aufgetreten ist und der zugehörige if
-Rumpf
setzt im Fehlerfall eine Fehlermeldung und gibt sie als Ergebnis zurück.
Geht hingegen alles gut, legt Zeile 54 den HTML-Inhalt von
Abbildung 3 in der Variablen $data
ab.
HTML::TableExtract
Das Modul HTML::TableExtract
ist nun ein ganz ausgefuchstes: Es
extrahiert die Daten aus Tabellen in HTML-Dokumenten. Dabei reicht es,
die Tabellen symbolisch anzugeben. Man sagt: ``Ich hätte gerne die
Tabelle mit den Spaltenüberschriften Kurs und Datum'' und schon
schlängelt sich der Parser durch das HTML-Dokument, sucht die richtige
Tabelle heraus und extrahiert daraus die angegebenen Spalten.
In unserem Fall geht es darum, aus dem wilden HTML-Code der Firma Yahoo
die in Abbildung 4 optisch hervorgehobenen Daten zu holen -- da
kommt HTML::TableExtract
gerade recht.
Abbildung 4: Die gesuchten Daten im HTML-Salat |
Nun enthält die Yahoo-Tabelle eine Besonderheit, nämlich eine
Spaltenüberschrift, die sich mit COLSPAN
über mehrere Kolumnen
erstreckt, was HTML::TableExtract
zur Drucklegung dieses Artikels
noch ins Schleudern brachte. Statt dessen fischt getquote.pl
in
Zeile 56 die Tabelle Nummer 4 (count => 4
)
im Dokument (depth
ist 0
, weil es
sich um keine Untertabelle, sondern um eine Tabelle auf höchster
Ebene im Dokument handelt) heraus, Zeile 58 wirft den Parser
an und Zeile 61 liest alle vom HTML befreiten Zeilen der gesuchten
Tabelle zeilenweise
in den Array @rows
ein. Zeile 62 entfernt die Spaltenüberschriften
und die foreach
-Schleife ab Zeile 63 iteriert über alle Tabellenreihen,
wobei Zeile 64 jeweils die erste und die fünfte Spalte extrahiert,
die den Aktiennamen und den aktuellen Kurs in Euro beinhalten. Abschließend
steht etwa folgendes in der Email an den Pager:
AOL.F 60,70 **
Wurde in Zeile 39 nicht F
als Börsenplatz eingestellt, sondern
etwa *
, antwortet Yahoo nicht nur mit einer, sondern mit vielen Zeilen,
die die Kurse an den verschiedenen Börsen angeben. In diesem Fall
schnappt sich die foreach
-Schleife eine Zeile nach der anderen
und gibt die Ergebnisse durch **
-Zeichen getrennt in einem String
zurück -- in Pagern muss Platz gespart werden.
Die verwendeten Module gibt's natürlich alle kostenlos auf dem CPAN.
LWP::UserAgent
und HTTP::Request::Common
sind Teile der
bekannten Bibliothek libwww, das
Internet::Mail
-Modul ist Teil der MailTools, für die verwendete
SMTP-Methode zum Mailen ist außerdem
das Net::SMTP
-Modul aus dem libnet-Bundle notwendig.
Am schnellsten wird alles wie immer mit Andreas Königs
CPAN
-Modul installiert:
perl -MCPAN -eshell cpan> install Net::SMTP cpan> install HTTP::Request::Common cpan> install Internet::Mail cpan> install HTML::Parser cpan> install HTML::TableExtract
Soweit die Module.
Damit das Ganze funktioniert, brauchen wir einen Rechner, der 24
Stunden am Tag am Internet hängt und Emails empfangen und senden kann.
Außerdem muss dort perl
installiert sein und ein sendmail
-ähnlicher
Mail-Daemon hausen.
Wer über keinen eigenen Rechner am Internet verfügt, kann sich mit einem Hosting-Service behelfen. Die besseren Services lassen nicht nur statische Webseiten und CGIs zu, sondern erlauben auch Shell-Zugriff auf Linux-Servern.
Damit das Skript bei eintreffender E-Mail sofort reagiert, muss
ein Eintrag in die .forward
-Datei unter dem Heimatverzeichnis
des entsprechenden Benutzers her:
$ cat /home/benutzer/.forward | /home/benutzer/bin/getquote.pl
Kommt E-Mail, die an
benutzer@irgendwo.com
gerichtet ist, für Benutzer
benutzer
auf dem entsprechenden Rechner an,
leitet sendmail
die Mail wegen der .forward
-Datei
an das Skript getquote.pl
weiter, welches die notwendigen Aktionen
wie oben beschrieben einleitet.
Ändert Yahoo entweder sein Eingabeformular oder das Ausgabeformat
grundlegend, müssen wir freilich die get_quote
-Funktion in
getquote.pl
umschreiben -- aber da wir nicht direkt auf
buchstabengetreue Wiedergabe angewiesen sind, sondern logische
Angaben machen (z.B. ``vierte Tabelle auf der Seite''), macht
es auch nichts aus, wenn Yahoo hin und wieder die Anzeigen
wechselt. Das vorgestellte Verfahren ist relativ robust,
aber, für alle Fälle: Wie immer wird die Online-Version des Skripts auf
www.linux-magazin.de stets auf dem neuesten Stand gehalten.
Viel Spaß beim Pagen -- aber wenn ich einen erwische, der seinen Pager im Kino oder im Restaurant klingeln lässt, dann raucht's! Bis zum nächstenmal!
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. |