Vorweg (allgemein)

Es hat eine ganze Weile gedauert, bis ich mich an WWW::Mechanize herangetraut habe. Eigentlich ist ausreichend Dokumentation vorhanden und Perl fasse ich eh ab und zu mal an. Mir hätten ein paar mehr konkrete Beispiele, gut dokumentiert, möglichst in meiner Muttersprache gut getan. Darum – weil es anderen vielleicht auch so geht – demonstriere ich hier ein Ergebnis meiner Studien.

In meinem konkreten Beispiel geht es um das Besorgen eines persönlichen Fahrplans für Verbindungen mit der Bahn oder anderer regionaler Anbieter im öffentlichen Personennahverkehr. Ich halte das Skript auch für diejenigen für sinnvoll, die nicht WWW::Mechanize lernen wollen – um sich Fahrpläne für eigene Verbindungen erstellen zu lassen, muss man aber trotzdem ein (ganz klein) wenig in die Materie einsteigen.

Ich hoffe, das Skript hilft dem einen oder der anderen beim Einstieg in WWW::Mechanize. Trotzdem eine Warnung vorweg: ich bin nicht gut im Scripten – und ganz bestimmt nicht in Perl; ich baue mir zurecht, was mir gerade einfällt. Das Teil funktioniert hier bei mir – das heißt aber noch lange nicht, dass es keinen Verbesserungsbedarf mehr gäbe, geschweige denn ich mich an gute technische Gepflogenheiten hielte.

Über Kommentare – vor allem Verbesserungsvorschläge – freue ich mich sehr.

Vorweg (speziell)

Für eine Fahrplanabfrage benötige ich zwei Dateien. Die erste Datei ist das Skript selber, welches die Abfrage insgesamt durchführt und die grundlegenden Einstellungen (wie zum Beispiel das gewünschte Format oder die E-Mail-Adresse) setzt. In der zweiten Datei stecken die Daten der jeweils speziellen Verbindung: Start- und Ziel-Bahnhof. Im vorgestellten Fall geht es um eine Verbindung von München nach Hamburg-Wandsbek.

Im Beispiel reicht dann auf der Kommandozeile ein besorgeFahrplan M-HH.Wandsbek um die Abfrage durchzuführen. Das kann man sich natürlich auch hübsch auf den Desktop legen – vielleicht objektorientiert, so dass man einfach die Datei mit den Verbindungsdaten per Maus auf das Skriptfile zieht und damit die Anfrage auslöst. Aber soweit bin ich noch nicht; ich benutze immer noch lieber die Kommandozeile.

Das Skript

Zuerst zum Skript selber, da steckt das meiste WWW::Mechanize drin. Zur Datei mit den Verbindungsdaten komme ich später, das ist dann nicht mehr aufregend.

Zur Umwandlung des Codes nach HTML habe ich source-highlight genutzt. Mit dem Aufruf source-highlight --src-lang=perl --line-number --out-format=xhtml --css=style.css besorgeFahrplan.

Konfigurieren

Zuerst konfiguriere ich mir die Anfrage zusammen:

022: my $StartURL = "http://persoenlicherfahrplan.bahn.de/bin/pf/query-p2w.exe/dn"; 023: 024: my %StandardAngaben = ( 025: 'weekday_mo' => 'checked', 026: 'weekday_tu' => 'checked', 027: 'weekday_we' => 'checked', 028: 'weekday_th' => 'checked', 029: 'weekday_fr' => 'checked', 030: 'weekday_sa' => 'checked', 031: 'weekday_su' => 'checked', 032: 'time0_from_hin' => '00:00', 033: 'time0_to_hin' => '23:59', 034: 'time0_from_rueck' => '00:00', 035: 'time0_to_rueck' => '23:59', 036: 'answerMode' => 'email', 037: 'eMailAddress' => '', 038: 'output' => 'pdb', 039: 'maxNrOfChanges' => '1000', 040: 'REQ0JourneyProduct_prod.0' => 'CHECKED', 041: 'REQ0JourneyProduct_prod.1' => 'CHECKED', 042: 'REQ0JourneyProduct_prod.2' => 'CHECKED', 043: 'REQ0JourneyProduct_prod.3' => 'CHECKED', 044: 'REQ0JourneyProduct_prod.4' => 'CHECKED', 045: 'REQ0JourneyProduct_prod.5' => 'CHECKED', 046: 'REQ0JourneyProduct_prod.6' => 'CHECKED', 047: 'REQ0JourneyProduct_prod.7' => 'CHECKED', 048: 'REQ0JourneyProduct_prod.8' => 'CHECKED', 049: 'REQ0JourneyProduct_prod.9' => 'CHECKED', 050: 'REQ0JourneyProduct_opt.1' => 'off', 051: 'REQ0JourneyProduct_opt.2' => 'off', 052: 'REQ0JourneyProduct_opt.3' => 'off', 053: 'outputFilter' => 'standard' 054: ); 055: 056: my $SentConnectionMessage = "Fahrplan wird Ihnen in etwa \(\\d+\) Minuten per eMail an"; 057: 058: #my $useProxy = 'http://127.0.0.1:8118/'; # Privoxy -> TOR 059: #my $useProxy = 'http://127.0.0.1:4001/'; # JAP 060: my $useProxy = ''; # no proxy 061: 062: my $FakeUserAgent = 'Windows IE 6'; 063: #my $FakeUserAgent = 'Windows Mozilla'; 064: #my $FakeUserAgent = 'Mac Safari'; 065: #my $FakeUserAgent = 'Mac Mozilla'; 066: #my $FakeUserAgent = 'Linux Mozilla'; 067: #my $FakeUserAgent = 'Linux Konqueror'; 068: #my $FakeUserAgent = ''; # don't fake 069: 070: # default level of output (if not set on command line) 071: my $optionVerbose = 1;

Zumindest die Zeilen 22 bis 54 sollten einfach zu verstehen sein. Zeile 22 bestimmt die URL, an welche ich die Anfrage richte und in den Zeilen 24 bis 54 lege ich meine Formular-Eingaben in ein Hash. WWW::Mechanize kann das so direkt verarbeiten und ich finde das an Übersichtlichkeit nicht zu überbieten.

In Zeile 37 müsst Ihr Eure E-Mail-Adresse eintragen, sonst läuft das Skript nicht, das versteht sich ja hoffentlich von selbst. Wenn Ihr den Fahrplan nicht per Mail haben wollt, müsste Ihr euch das Skript – vor allem im hinteren Teil – umschreiben. In der vorliegenden Version wird es per Mail geschickt (siehe dazu Zeile 36).

Um an die Formularfelder heranzukommen hilft Euch auf der ersten Seite das WWW::Mechanize beiliegende mech-dump. Ein kurzes mech dump http://persoenlicherfahrplan.bahn.de/bin/pf/query-p2w.exe/dn zeigt Euch die Formularfelder der Seite an sowie die möglichen einzutragenden Werte. Das ist manchmal etwas frickelig, aber im Vergleich mit dem Code hier findet Ihr bestimmt schnell raus wie die Angeben zu verstehen sind.

Leider hilft mech-dump nicht über alle Hürden. Ich habe eine Beispielverbindung gewählt, deren Ziel nicht eindeutig ist und deshalb auf einer zweiten Seite nochmal spezifiziert werden muss. Da kommt Ihr nicht per mech-dump ran. Ihr müsst dann leider in den HTML-Quelltext gucken. Ein bisschen Vorbildung in diesem Bereich ist da leider unabdingbar.

Weiter im Code: Zeile 56. Mit diesem String bestätigt der Bahn-Server den Erfolg der Anfrage. Die Dauer derselben wird per regulärem Ausdruck abgegriffen. Die will ich nämlich für die Ausgabe des Skriptes (kommt später) haben.

In den Zeilen 58 bis 68 steckt ein wenig Spielerei. Ich kann die Anfrage über einen Proxy führen und den User-Agent, der an den Server übermittelt wird, faken. Über die Sinnhaftigkeit sowohl des einen als auch des anderen will ich an dieser Stelle nicht diskutieren: es geht mir lediglich um die Darstellung der Fähigkeiten von WWW::Mechanize.

Zum Nutzen bzw. Abstellen dieser Einstellungen müsst Ihr bloß die Kommentarzeichen am Zeilenanfang richtig setzen.

Zeile 71 hat weder mit der Bahn zu tun noch mit WWW::Mechanize. Ich bestimme damit lediglich die Gesprächigkeit des Skriptes. "1" informiert ein wenig über den Fortgang der Anfrage. "2" ist etwas detaillierter und mit "0" kann man die Ausgabe ganz abstellen. Dieser Wert lässt sich auf der Kommandozeile ändern.

Programmvorbereitung

077: my (@AlleGewuenschtenVerbindungen,$AktuelleVerbindung); 078: our @VerbindungsAngaben; 079: 080: my $mech = WWW::Mechanize->new( autocheck => 1 ); 081: $mech->proxy(['http', 'ftp'], $useProxy) if $useProxy; 082: $mech->agent_alias($FakeUserAgent) if $FakeUserAgent;

Bevor es losgeht, noch ein wenig Kleinkram: Zeilen 77 und 78 betreffen über das ganze Skript gültige Variablen. In @AlleGewuenschtenVerbindungen stecken alle auf der Kommandozeile übergeben Verbindungsdateien (damit man sich mit einem Aufruf gleich mehrere Fahrpläne besorgen lassen kann) und in $AktuelleVerbindung steckt die, die während des Programmablaufs gerade aktuelle bearbeitet wird. @VerbindungsAngaben bekommt ein our, weil diese Variable in der externen Datei befüllt wird.

im Folgenden werde ich hier weniger die Innereien des Skriptes referieren, sondern mich hauptsächlich auf die Teile zu WWW::Mechanize beschränken. Die allgemeinen Teile sind (hoffentlich) im Quelltext ausreichend dokumentiert,

Nun endlich, in Zeile 80, kommt WWW::Mechanize zum Einsatz. Ich setze autocheck => 1. Sollten Netzzugriffe im Skript scheitern, bricht WWW::Mechanize die Verarbeitung ab. Ich muss mich im Skript also nicht selbst um die Fehlerbehandlung kümmern.

In den Zeilen 81 und 82 werden die oben gemachten Einstellungen – so es denn welche gibt – zu Proxy und User-Agent an WWW::Mechanize übergeben.

Verbindungsangaben aus externem File lesen

153: # Daten der aktuellen Verbindung in Array einlesen 154: my @Verbindungsdaten = readConnectionData($AktuelleVerbindung); 155: # Anzahl der auszufüllenden Webseiten herausfinden 156: my $AnzahlSeiten = scalar(@Verbindungsdaten);

Jetzt doch noch ein wenig Skript-Interna: Zeile 154 liest die Daten zu Start- und Zielbahnhof aus der dem Skript auf der Kommandozeile übergebenen Datei aus. In Zeile 156 wird ausgelesen, wie viele Seiten dazu ausgefüllt werden müssen. In der Regel reichen dazu die Formular-Angaben auf der ersten Seite. Es kann aber durchaus vorkommen, dass sich partout nicht der exakt passende Eintrag finden lässt und der Bahn-Server eine genauer spezifizierte Angabe haben möchte und auf einer zweiten Seite ein Auswahlliste vorschlägt. Wieviele Seiten insgesamt auszufüllen sind, wird in $AnzahlSeiten gespeichert. Mehr als zwei sind mir dabei aber auch noch nicht unter gekommen.

Es geht los

Seite holen

158: # Get first webpage 159: print "-- Hole $StartURL ..." if $optionVerbose > 1; 160: $mech->get($StartURL); 161: # show results 162: if ( $mech->success ) { 163: print " erledigt.\n" if $optionVerbose > 1; 164: } 165: else { 166: print " failed:\n"; 167: print "--- $mech->response->status_line;\n\n"; 168: }

Endlich tut sich etwas: in Zeile 160 wird die Bahn-Seite vom Server geholt und getestet, ob die Anfrage erfolgreich war. Wenn es eine Fehlermeldung gegeben hat, wird diese in Zeile 164 ausgegeben. Der else-Abschnitt ist ein Überbleibsel der ersten Version des Skriptes, als ich den Wert von autocheck noch auf "0" gesetzt hatte (siehe Zeile 80) – da in der aktuellen Fassung des Skriptes WWW::Mechanize die Fehlerbehandlung selbst übernimmt, ist der else-Fall nicht mehr von Bedeutung.

Formular ausfüllen

175: # Daten der aktuellen Seite in Hash einlesen 176: my %Seiteneingaben = %{$Verbindungsdaten[$i]}; 177: 178: # und ausfüllen 179: $mech->form_number(4); 180: 181: # die Standardfelder nur auf der ersten Seite allerdings 182: if ( $i < 1 ) { 183: print "--- Fuelle Standard-Felder aus ..." if $optionVerbose > 1; 184: $mech->set_fields( %StandardAngaben ); 185: print " erledigt.\n" if $optionVerbose > 1; 186: } 187: # und die Felder für diese spezielle Verbindung eintragen 188: print "--- Fuelle Felder fuer Verbindungsdaten aus ..." if $optionVerbose > 1; 189: $mech->set_fields( %Seiteneingaben ); 190: print " erledigt.\n" if $optionVerbose > 1; 191: # und abschicken 192: print "--- Formular abschicken ..." if $optionVerbose > 1; 193: $mech->click_button('name' => 'start'); 194: # show results 195: if ( $mech->success ) { 196: print " erledigt.\n" if $optionVerbose > 1; 197: } 198: else { 199: print " failed:\n" if $optionVerbose > 1; 200: print "--- $mech->response->status_line;\n\n"; 201: }

Jetzt zu dem, was WWW::Mechanize ausmacht: Formulare ausfüllen. In Zeile 176 werden die Daten der aktuellen Verbindung für die gerade zu bearbeitende Seite (also in der Regel der ersten, selten der zweiten – der mit der Auswahlliste bei mangelnder Eindeutigkeit eines Bahnhofes) im Hash %Seiteneingaben gespeichert.

Zeile 179 weist WWW::Mechanize an, die folgenden Werte in das zweite Formular der Seite einzutragen (das erste Formular – was Ihr ja mit z.B: mech-dump – herausgefunden habt) ist für die Sprachwahl der Seite zuständig.

Zeile 184 setzt die im Konfigurationsabschnitt des Skriptes festgelegten Standardwerte und Zeile 189 die für die angefragte Verbindung auf der gerade zu bearbeitenden Seite notwendigen Daten.

In Zeile 193 schließlich wird das ausgefüllte Formular an den Server abgeschickt – und auf Erfolg geprüft wird. Falls diese ausbleibt, endet die Abarbeitung des Skript mit einer Ausgabe des Fehlers in Zeile 200.

Dieser Teil steckt in einer Schleife, die dann beendet ist, wenn alle Anfrageseiten abgearbeitet sind. Üblicherweise schickt nun der Bahn-Server die Seite mit der Erfolgsmeldung zum erstellten Fahrplan zurück.

Ergebnis verarbeiten

204: # store webpage in scalar and save the base URL 205: $Seite = $mech->content('base_href' => undef); 206: 207: # look for string that indicates the success of request 208: $Seite =~ /$SentConnectionMessage/; 209: # store the given time for arrival of requested file 210: my $Minuten = $1; 211: 212: # show time to wait for the requested file ... 213: if ($Minuten) { 214: print "- Erfolg: Der Fahrplan wird in $Minuten Minuten gemailt." if $optionVerbose > 0; 215: } 216: # or that the request failed (in this case save the page in a file) 217: else { 218: open(FILE,"> result-$AktuelleVerbindung.html") or 219: die "- Konnte Datei \"result-$AktuelleVerbindung.html\" nicht öffnen\n"; 220: print FILE $Seite; 221: close(FILE); 222: print "- Es stimmt was nicht. Bitte result-$AktuelleVerbindung.html anschauen!"; 223: }

WWW::Mechanize soll sich auch um das Ergebnis kümmern und ausgeben, wie lange die Erstellung des Fahrplanes dauern soll. Dazu wird die vom Bahn-Server zurückgegebene Seite in Zeile 205 in $Seite gespeichert und in Zeile 208 auf den im Konfigurationsabschnitt des Skriptes bestimmten String getestet. Ist dieser vorhanden, wird die Dauer der Erstellung extrahiert und in Zeile 214 ausgegeben.

Ist der entsprechende String nicht vorhanden, wird die vom Bahn-Server zurückgegebene Seite in den Zeilen 218 bis 221 zur Analyse in eine Datei gespeichert (vielleicht wurde ja bloß der Satz geändert) und in Zeile 222 über den Fehlschlag auf der Kommandozeile informiert.

Damit ist das Skript beendet und die Anfrage sollte mit dem Eintreffen des Fahrplanes auf Euren Mail-Konto nach $Minuten Minuten abgeschlossen sein.

Die Verbindungsdaten

Wie oben erwähnt, benötigt das Skript eine externe Datei, in welcher die Angaben einer konkreten Verbindung gespeichert sind. Ich lasse mir in diesem Beispiel einen Fahrplan für die Strecke von München nach Hamburg erstellen. (In meiner Erinnerung ging es in unseren Schulbüchern früher in solchen Fällen immer um die Strecke München–Hamburg.) Um zu demonstrieren, wie das mit der Auswahlliste auf der Folgeseite funktioniert, weil die Bahnhofsangabe auf der ersten Seite nicht eindeutig genug für den Bahn-Server war, habe ich als Zielbahnhof »Wandsbek« ausgesucht.

01: @VerbindungsAngaben = ( 02: { # Seite 1 03: 'REQ0JourneyStopsS0G' => 'München', # Startbahnhof 04: 'REQ0JourneyStopsZ0G' => 'Hamburg Wandsbek' # Zielbahnhof 05: }, 06: { # Seite 2 07: 'REQ0JourneyStopsZ0K' => 'S-1N4' # Auswahlfeld Ziel (Bahnhof Wandsbek) 08: } 09: ); 10: 11: 1; 12: 13: ### Local Variables: ### 14: ### mode: perl ### 15: ### coding: latin-1 ### 16: ### End: ###

Was auf den ersten Blick ganz nett aussieht, erweist sich bei näherem Hinsehen als doch etwas verwickelt. Die Klammern nämlich. Ich habe lange gedacht, dass es sich bei @VerbindungsAngaben um ein Array aus Hashes handelt. Peter wies mich aber per Mail darauf hin, dass es sich stattdessen um ein Array (eine Liste) von Hash-Referenzen handelt. Man kann – glaube ich – ganz gut sehen wie es funktioniert: die Angaben in den geschwungenen Klammern sehen genauso aus wie diejenigen im Konfigurationsteil des Skriptes. Und von diesem in geschwungenen Klammern steckenden Abschnitten gibt es genau zwei. Einen für die erste Seite der Anfrage und einen für die zweite Seite mit der Auswahlliste für die nicht eindeutige Bahnhofsangabe. Darum auch der komische Wert S-1N4 – das ist einfach der vierte Eintrag in der Liste. (Das müsst Ihr bei Euren eigenen Versuchen leider mit einem Blick auf den HTML-Quelltext der Seite herausfinden.)

Wenn – was in den meisten Fällen der Fall sein dürfte – zur Bearbeitung der Anfrage die Daten der ersten Seite reichen, entfallen ganz einfach die Zeilen 5 bis 7. (Eigentlich sind es natürlich die Zeilen 6 bis 8 – aber dann müsste ich noch extra darauf hinweisen, dass das Komma am Ende von Zeile 5 noch weg muss; und das war mir einfach zu stressig.)

Noch ganz kurz zu den Zeilen 13 bis 16, speziell Zeile 15 der Datei. Das sind Angaben für den Emacs. Zeile 15 weist diesen an, die Datei bei Speichern in ISO-8859-1 zu kodieren. Versuche haben gezeigt, dass es mit UTF-8 aber auch keine Probleme gibt.

Download

Ich habe sowohl das Skript als auch die Datei mit den Verbindungsdaten für München–Hamburg in ein ZIP-File gepackt.

Changelog

12.12.2011: Formularnummer in Zeile 179 an die Veränderungen auf der Bahnseite angepasst (jetzt: 3).

16.12.2008: Formularnummer in Zeile 179 an die Veränderungen auf der Bahnseite angepasst (jetzt: 4).

8.10.2008: Erklärung zu @VerbindungsAngaben: Es handelt sich nicht um ein Array aus Hashes, sondern um ein ein Array von Hash-Referenzen. Dank an Peter.

1.10.2008: In den Standardangaben im Skript wurden die Keys für die Verkehrmittel an die geänderten Bezeichnungen des Formulars angepasst.
Der autocheck-Wert wird per default auf "1" gesetzt.

3.10.2007: Im Verbindungsdatenfile wurden die Keys für An- und Abfahrtbahnhöfe an das geänderte Formular bei der Bahn angepasst. Ebenso der Wert im Auswahlfeld auf Seite 2.

Webimpressum · 08.10.2008