Bildschirm-Abzieher (Linux-Magazin, März 2004)

Die WWW::Mechanize-Shell hilft, sogenannte Screen-Scraper zu schreiben, die wie Browser agieren und den Zugriff auf Webseiten automatisieren.

In Kalifornien darf man sich das Nummernschild für's Auto gegen eine Gebühr selbst aussuchen. Aus naheliegenden Gründen führt mein 13 Jahre alter Acura Integra das Kennzeichen ``PERL MAN'' (Abbildung 1). Die Kraftfahrzeugbehörde gibt sich fortschrittlich und bietet eine Webseite an, die prüft, ob das gewünschte Kennzeichen verfügbar ist -- allerdings geht mir das viel zu langsam. Heute untersuchen wir mal, wie man einen doch recht komplexen Web-Flow in Perl-Geschwindigkeit nachbildet.

Abbildung 1: Der erstaunliche PERL MAN: Schon 13 Jahre alt, kann es aber nicht lassen, die Sportwagen an den Ampeln San Franciscos zu provozieren.

Für solche Screen-Scraper-Projekte hat Andy Lester das Modul WWW::Mechanize entwickelt. Neuerdings steht mit WWW::Mechanize::Shell von Max Maischein sogar eine Shell zur Verfügung, mit der man eine Browser-Session sehr schnell in ein beliebig oft reproduzierbares Skript umwandelt. Die Beschreibung der Session erfolgt auf logischer Ebene (z.B. ``springe zum einem Link auf der Seite, der den Text 'xxx' enthält''), sodass das Skript auch dann noch funktioniert, wenn das Format der Webseite sich ändert.

Eine CPAN-Shell installiert schnell die Module WWW::Mechanize und WWW::Mechanize::Shell, die ihrerseits von weiteren Modulen abhängen, was allerdings dank der Shell-Automatik komfortabel von statten geht. Die Browser-Emulator-Shell startet dann mit

    perl -MWWW::Mechanize::Shell -eshell

und wartet mit einem Prompt auf interaktive Benutzereingaben. Bei der Installation zieht das Modul noch einige Abhängigkeiten herein und benötigt IO::Socket::SSL, um mit https-Seiten zurechtzukommen.

Um die Seite der kalifornischen Kraftfahrzeugstelle Department of Motor Vehicles zu laden, tippt man einfach

    >get http://www.dmv.ca.gov

ein und die Shell antwortet

    Retrieving http://www.dmv.ca.gov(200)

um zu signalisieren, dass die Startseite mit Code 200 (OK) erfolgreich geladen wurde.

Abbildung 2: Die Eingangsseite des kalifornischen Department of Motor Vehicles mit dem Link auf die personalisierten Nummernschilder.

Die Browser-Darstellung (Abbildung 2) zeigt den gesuchten Link zu Personalized Plates. Um herauszufinden, welche Links die Shell auf der Seite erkannt hat, genügt das Kommando

    >links

das alle verfügbaren Links anzeigt:

    ...
    [14] Vehicle Industry & Commercial Permits
    [15] Personalized Plates
    [16] Disabled Placards
    ...

Nun könnten wir einfach Link Nummer 15 mit

    open 15

folgen, aber das wäre nur begrenzt gültig, da die Anzahl und Reihenfolge der Links auf einer regelmäßig gewarteten Seite sich stetig ändern. Um den Vorgang zu generalisieren und auch dann noch funktionsfähig zu halten, falls die Behörde neue Links in die Seite einfügt, sucht folgender Befehl einfach nach einem Link, dessen Text-Beschreibung Personlized enthält und ``klickt'' auf ihn:

    open /Personalized/

Passen auf den angegebenen regulären Ausdruck mehrere Links der Seite, stellt die Shell ein Auswahlmenü dar. Im vorliegenden Fall passt nur einer, die Shell quittiert dies mit

    Found 15
    (200)

und befindet sich nun auf einer Seite, die weitere Links anzeigt, unter anderem einen, der den Text ``order Special Interest Plates'' enthält. Die Suche nach dem regulären Ausdruck (wichtig: Anführungszeichen wegen der Leerzeichen):

    open "/order Special Interest/"

führt zu einer weiteren Seite, auf der sich mehrere HTML-Formulare befinden, die sich einfach durch den Befehl

    forms
    ...
    Form [2]
    POST https://vrir.dmv.ca.gov/ipp/PerLicensePlateServlet [personalized]
      page=Select                     (hidden)  
      Submit2=Order Personalized      (submit)  
    ...

anzeigen lassen. Das zweite (mit der Bezeichnung ``personalized'') ist das interessante, das wir dem Namen nach auswählen und auf den Submit-Knopf drücken:

    form "personalized"
    submit

Nun zeigt die Behörde ein weiteres Formular, um mit Drop-Down-Menüs und Radio-Buttons Fahrzeugtyp, Nummernschildmuster und ähnliches auszuwählen. Für solche komplizierten Formulare eignet sich der fillout-Modus der Shell, der die Werte für die einzelnen Formularfelder interaktiv vom Benutzer entgegennimmt, mit dem Kommando fillout startet, und einen Dialog nach Abbildung 3 führt. Der Radio-Button bleibt deaktiviert, weil einfach die Enter-Taste ohne weitere Eingabe gedrückt wird. Ein abschließender submit-Befehl veranlasst die Shell, die eingestellten Werte an den Server zu schicken.

Abbildung 3: Der Befehl fillout in der WWW::Mechanize::Shell nimmt vom Benutzer Werte für die einzelnen Felder entgegen

Endlich befinden wir uns nun auf der Seite, die die Auswahl des Nummernschilds zulässt. Wie die Browser-Darstellung in Abbildung 4 zeigt, erfolgt dies wiederum über ein HTML-Formular, das die Buchstaben des Nummernschildes einzeln (!) in Auswahlboxen abfragt. Wiederum erfüllt ein fillout seinen Zweck und ein submit schickt die Daten.

Abbildung 4: Auf der Webseite des DMV kann man sein zukünftiges Nummernschild begutachten.

Ra-Ru-Rick Barbatrick!

Und nun kommt der Clou: Um aus dieser ``Session'' ein Skript zu generieren, das man an die privaten Erfordernisse anpassen und beliebig oft ablaufen lassen kann, genügt der Befehl

    script

in der Shell! Kaum eingetippt, schon spuckt die treue Seele ein Perl-Skript aus, das genau das reproduziert, was bisher in Handarbeit eingegeben wurde.

Listing dmv ist so entstanden. Freilich wurde es noch angepasst: So kann man nun das zu untersuchende Nummernschild auf der Kommandozeile mit etwa

    dmv PERLMAN

angeben. Zeile 12 bricht ab, falls dort nichts vorliegt. Der Konstruktor des WWW::Mechanize-Objekts mit gesetzter autocheck-Option betreibt den Browser-Simulator in einem Modus, der sofort das Programm abbricht, falls eine Webseite nicht gefunden wird oder sonst etwas schiefgeht.

Zeile 19 dirigiert den Simulator zur Eingangsseite. Die follow()-Methode in Zeile 20 springt wegen des angegebenen regulären Ausdrucks auf einen Link, der den Text Personalized enthält. Zeile 21 führt einen weiteren, regex-gesteuerten Sprung durch. Zeile 23 wählt das Formular mit dem Bezeichner personalized aus und die darauffolgende submit()-Methode drückt auf den Submit-Knopf des sonst leeren Formulars.

Das in Zeile 17 erzeugte WWW::Mechanize::FormFiller-Objekt übernimmt das Ausfüllen des nun erscheinenden Formulars. Die add_filler()-Methode spezifiziert jeweils den Namen eines Formularfeldes, das Eingabeverfahren und den Wert:

    $fi->add_filler('leased' => Fixed => 'N' );

Für hartkodierte Werte findet Fixed Anwendung, Interactive weist den FormFiller, den Benutzer zur Laufzeit nach dem entsprechenden Wert zu fragen.

Weiss der FormFiller, woher er alle Werte für die Formularvariablen herbekommt, kann's losgehen und die fill_form-Methode startet mit dem gegenwärtig bearbeiteten HTML::Form-Objekt des WWW::Mechanize-Agenten den Ausfüllvorgang:

    $fi->fill_form($agent->current_form);

Die anschließend abgeschickte submit-Methode sendet die Werte an den Server.

Das Skript wird die Eingangsseite des DMV ansteuern, sich geduldig durch die ganzen Links und Formulare wühlen, schließlich in der for-Schleife ab Zeile 39 die angegebene Zeichenkette in die Auswahlboxen eintragen. Enthält das vorgeschlagene Nummernschild weniger als 7 Buchstaben, setzt die Logik ab Zeile 42 einfach leere Felder ein. Der simuliert-gedrückte Submit-Button in Zeile 54 schickt die Daten per SSL an den Server und die Shell nimmt das die Ergebnisseite als HTML entgegen.

Es kann nur einen geben

Steht dort etwas wie ``not available'', ist die Kombination entweder schon vergeben oder wegen anstößigen Inhalts nicht verfügbar und das Skript gibt eine entsprechende Kurzmeldung aus. Wurde das Kennzeichen genehmigt, erscheint ein Bestellformular, welches das Skript anhand der Zeichenkette Complete Order Form erkennt und deswegen die Meldung XXX available ausspuckt. Die Ausgabe

    PERLMAN: not available

bestätigt, dass PERL MAN nun leider schon vergeben und wohl auf lange Sicht nicht verfügbar ist -- aber nutzt die Funktionalität der Shell ruhig, um mühelos Perl-Skripts zu erzeugen, die langwierige Zugriffe auf Webseiten automatisieren!

Listing 1: dmv

    01 #!/usr/bin/perl
    02 ###########################################
    03 # dmv -- Automate checking CA plates
    04 # Mike Schilli, 2003 (m@perlmeister.com)
    05 ###########################################
    06 use strict;
    07 use warnings;
    08 
    09 use WWW::Mechanize;
    10 use WWW::Mechanize::FormFiller;
    11 
    12 die "usage: $0 XXXXXXX" unless 
    13                           defined $ARGV[0];
    14 $ARGV[0] =~ s/\s+//g;
    15 my $agent = WWW::Mechanize->new(
    16               autocheck => 1);
    17 my $fi = WWW::Mechanize::FormFiller->new();
    18 
    19 $agent->get('http://www.dmv.ca.gov');
    20 $agent->follow(qr(Personalized));
    21 $agent->follow(
    22      qr(order Special Interest));
    23 $agent->form("personalized");
    24 $agent->submit();
    25 
    26 $fi->add_filler('vehicletype' => 
    27                 Fixed => 'AUTO' );
    28 $fi->add_filler('leased' => 
    29                 Fixed => 'N' );
    30 $fi->add_filler('platetype' => 
    31                 Fixed => 'R' );
    32 $fi->add_filler('kidpic' => 
    33                 Fixed => '' );
    34 $fi->add_filler('Submit2' => 
    35                 Fixed => '' );
    36 $fi->fill_form($agent->current_form);
    37 $agent->submit();
    38 
    39 for(0..6) {
    40     $fi->add_filler("LicPltCharAry$_" => 
    41          Fixed => 
    42          $_ > length $ARGV[0] ?
    43          "" : substr($ARGV[0], $_, 1));
    44 }
    45 
    46 for(0..6) {
    47     $fi->add_filler("HalfSpace$_" => 
    48                     Fixed => '');
    49 }
    50 
    51 $fi->add_filler('Submit2' => Fixed => '');
    52 $fi->fill_form($agent->current_form);
    53 
    54 $agent->submit();
    55 
    56 if($agent->content() =~ /not available/) {
    57     print "$ARGV[0]: not available\n";
    58 } elsif($agent->content() =~ 
    59                   /Complete Order Form/) {
    60     print "$ARGV[0]: available\n";
    61 } else {
    62     print "Unexpected response", 
    63           $agent->content(), "\n";
    64 }

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2004/03/Perl

[2]
Webseite des kalifornischen Department of Motorvehicles zur Auswahl von personalisierten Kennzeichen: http://www.dmv.ca.gov

Michael Schilli

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