XML eignet sich nicht nur zum Auszeichnen von Webdokumenten sondern ganz allgemein zum Strukturieren beliebiger Datensammlungen. Wie wär's mit einem automatischen Formularausfüller, der seine Weisheit aus XML-Daten bezieht?
Neulich, während ich eine auf Formulardaten reagierende Web-Applikation schrieb und zum Testen zum zehnten Mal die Felder ausfüllte, rief ich aus: Es reicht! Es müsste doch möglich sein, einem Perl-Skript vorzuschreiben, welche Daten ins Formular einzutragen wären, bevor es die Antwort des Servers vom Netz holt.
Nun hat der gute Herr Schwartz ja in [1] einen raffinierten Formulargrabscher vorgestellt, der aus den Feldern eines Online-Zettels ein Perl-Skript generiert, welches man editieren und zum Testen verwenden kann. Ich wollte allerdings etwas konservativeres: Ausgehend von einer formalen Beschreibung eines Requests soll mein serviler Agent ein Formular auf einem Webserver ausfüllen. In einer Datei lege ich fest, welchen URL, welche Methode (GET/POST), welche Parameter einfließen (d.h. welche Werte der Agent in welche Felder des Formulars eingibt), welche sonstigen Statusinformationen vorliegen (z.B. Cookies) und welche Sonderdinge zu beachten sind (Proxy-Einstellungen, passwortgeschützte Bereiche).
Das sieht sehr nach einem Programm mit unzähligen Kommandozeilenoptionen aus -- oder nach einer Beschreibungssprache. Um nicht schon wieder ein neues Format zu erfinden, sprang ich kurzentschlossen auf den letztens unüberhörbar bimmelnden XML-Zug.
Mit XML, der eXtensible Markup Language, lassen sich Teile eines Dokuments ``auszeichnen'', also mit einer Markierung versehen:
Eine <wichtig>flexible</wichtig> Sprache
Anders als HTML mit seinem fest vordefinierten
Satz von Markierungen (<h1>
, <table>
, etc.)
läßt XML den Anwender beliebig viele neue Tags definieren.
Es gibt keine ``richtigen'' Tags in XML, jeder kreiert nach Lust und
Laune seinen eigenen
Markup. So eignet sich XML nicht nur zum Strukturieren von Web-Dokumenten
sondern auch für beliebige Datensammlungen.
Syntaktisch ist XML strenger als HTML und so müssen alle öffnenden Tags auch wieder geschlossen werden:
<tag> ... </tag>
Eine Ausnahme bilden lediglich ``leere'' Tags, die keinen Inhalt aufweisen -- für sie ist wahlweise die Kurzschreibweise
<tag/>
gültig. Schachtelungen treten nur in der richtigen Reihenfolge auf, d.h.
<aussen> <innen> </aussen> </innen>
ist falsch, denn was zuerst geöffnet wurde, muß auch zuerst wieder geschlossen werden, wie in folgendem gültigen Konstrukt:
<aussen> <innen> </innen> </aussen>
Attribute, die man in HTML schlampig als
<a href=http://...>
hinpfefferte, müssen in XML in Anführungszeichen:
<anker url="http://...">
Groß- und Kleinschreibung spielen eine Rolle,
<TAG>
und
<tag>
sind unterschiedliche Tags, und um Verwirrungen zu vermeiden
schreiben wir heute mal alles klein.
Listing simple.xml
zeigt den einfachsten Fall eines Requests: Einfach
ein URL eines Dokuments, das vom Netz geholt wird. Der
<request>
-Tag ist in diesem Fall leer, es reicht,
einfach als Attribut den URL anzugeben.
Listing advanced.xml
zeigt die XML-Definition eines Requests mit
zwei zusätzlich definierten Parametern und zwei HTTP-Headern.
Weiter steht das method
-Attribut auf POST
, worauf der
Request
die Parameter, anders als als bei der standardmäßig
eingestellten GET
-Methode, nach der POST
-Methode an
den Webserver übergibt.
Es handelt sich um einen HTTP-Request auf das in [2]
vorgestellte Skript dump.cgi
, das testhalber einfach die
hereinkommenden CGI-Parameter ausgibt.
Abbildung 2 zeigt, was dump.cgi
auf den Request hin ausspuckt: Nicht nur die zwei Parameter, die
nach der POST-Methode übergeben wurden, kommen an, sondern auch
die beiden Spezial-Header X-Header1
und X-Header2
.
Listing password.xml
zeigt die Definition eines Requests, der auf einen
passwortgeschützten Bereich des Webservers zugreift. Auch
soll ein Proxy aushelfen, im vorgestellten Fall mein Squid, der
auf dem lokalen Rechner auf Port 3128 läuft.
Damit wäre unsere Markup-Sprache auch schon erklärt: Ein request
führt als vorgeschriebenes Attribut url
, zusätzlich dürfen
optional method
und proxy
definiert werden.
Ein request
kann entweder leer sein oder ein
oder mehrere parameter
, header
oder credentials
-Elemente enthalten.
parameter
- und header
-Elemente führen als Pflichtattribute name
und
value
, ein credentials
-Element hat username
und password
.
Diese Beschreibung der Sprache lässt sich computerfreundlich in eine
DTD (Document Type Definition) verpacken, die ein validierender Parser
benutzt, um die Gültigkeit eines XML-Texts zu überprüfen. Listing
request.dtd
zeigt die Definition für unsere Request-Sprache.
Aber wie holen wir, ausgehend von der Beschreibung mit dem XML-Markup
das Dokument vom Netz? Für Aufgaben im WWW gibt's glücklicherweise
schon ein Modul auf dem CPAN, den LWP::UserAgent
, nur versteht der
leider kein XML, sondern besteht darauf, dass wir die Parameter über
Methoden eingeben. Kompliziert wird die Lage dadurch, dass eigentlich
zwei verschiedene Objekte im Spiel sind, der LWP::UserAgent
, dem
man Proxy- und Passwortinformationen mitgibt und das
HTTP::Request
-Objekt,
das man dem LWP::UserAgent
überreicht und das den URL,
die GET oder die POST-Methode, die Parameter und die Header festlegt.
Was tun? Wie immer in solchen Fällen, in denen wir
nur Teile eines Moduls
ändern müssen, um die neue Funktionalität zu erhalten, kommt Vererbung
zum Einsatz. So zeigt Listing XmlUserAgent.pm
eine von
LWP::UserAgent
abgeleitete Klasse XmlUserAgent
, die
die Methoden get_basic_credentials
und request
redefiniert und
einen Konstruktor für die abgeleitete Klasse bereitstellt.
Verschieben wir die Implementierung von XmlUserAgent.pm
mal auf
später -- die Anwendung geht wie von LWP::UserAgent
gewohnt:
use XmlUserAgent;
$ua = XmlUserAgent->new(); $response = $ua->request($xmltext);
print $response->content();
$xmltext
enthält den Request in XML, die Fehlerbehandlung
wurde einmal ausgespart.
Das Skript fetch.pl
, das in Listing 5 vorgestellt ist, nimmt
Namen von Requestdateien auf der Kommandozeile entgegen:
fetch.pl simple.xml advanced.xml
führt die Requests aus und gibt für jeden einzelnen
das Ergebnis aus, welches aus den vom Server gesandten Headern und
dem Inhalt des zurückgegebenen Dokuments besteht.
Cookies werden von Request zu Request automatisch durchgeschleift.
Hierzu kommt in Zeile 4 von fetch.pl
das Modul HTTP::Cookies
herein,
das eine komfortable Schnittstelle zur Cookie-Kontrolle mit dem
LWP::UserAgent
bietet: Einmal, wie in Zeile 9, die
cookie_jar
-Methode mit einem neu erzeugten HTTP::Cookies
-Objekt
aufgerufen, und schon verhält sich der UserAgent wie ein
handelsüblicher Browser mit aktivierten Cookies.
Die Zeilen 12 bis 14 lesen den Inhalt der aktuellen XML-Datei aus
und legen ihn in $data
ab. Zeile 16 feuert den Request ab, das
Ergebnis liegt anschließend als HTTP::Response
-Objekt in
$response
, das mit den Methoden is_success
, content
,
headers
, code
, message
nach Erfolgsstatus, Dokumentinhalt, empfangenen HTTP-Headern,
Fehlercode und Fehlermeldung befragt werden darf.
Die Zeilen 18 bis 25 ahmen einen Browser-Bug nach:
Laut HTTP-Spezifikation soll der Browser, falls er auf einen POST-Request
einen Redirect erhält, diesem nicht automatisch
folgen, was LWP::UserAgent
auch geflissentlich so implementiert -- nur machen's alle wichtigen
Browser anders und folgen dem Redirect.
Nun zu XmlUserAgent.pm
:
Der @ISA
-Array in Zeile 10 legt
XmlUserAgent
als einen Spezialfall von LWP::UserAgent
fest, nur
dass die request
-Methode von XmlUserAgent
nicht nur mit
einem HTTP::Request
-Objekt wie sein großer Bruder arbeitet, sondern
als Argument auch wahlweise ein Stück XML-Text im oben definierten
Format versteht.
Da perl
beim Erzeugen einer abgeleiteten Klasse nicht automatisch
den Konstruktor der Basisklasse aufruft, muss new
selbst dafür sorgen: Die Instanzvariable ua
zeigt auf das Objekt
der Basisklasse, deren Konstruktor mit dem SUPER
-Konstrukt aufgerufen wird.
Das SUPER
-Schlüsselwort
läßt perl
die Basisklasse der gegenwärtigen Klasse suchen,
um dann dort die gewählte Methode auszuführen,
$class->SUPER::new()
ist also im Beispiel
gleichbedeutend mit LWP::UserAgent->new()
.
Die weiter definierten Instanzvariablen nehmen alle im XML-Text definierten
Parameter auf. Im Konstruktor werden sie auf
auf unverfängliche Werte gesetzt,
url
und method
auf undef
, genau wie username
und password
,
die beiden Variablen für den Fall,
dass eine Webseite passwortgeschützt ist.
Die Listenreferenzen
headers
und params
zeigen auf anfangs leere Listen.
Der bless
-Befehl in Zeile 26 schließlich macht aus dem
anonymen Hash mit den Instanzvariablen ein Objekt der
Klasse XmlUserAgent
.
Die request
-Methode ab Zeile 39 überprüft zunächst, ob
als Request ein Objekt der Klasse HTTP::Request
vorliegt.
Ist dies der Fall, liefert ref $req
den Klassennamen
zurück, und Zeile 43 ruft statt unserer Fancy-Logik die
ganz normale request
-Methode des LWP::UserAgent
auf.
In diesem Fall imitiert XmlUserAgent
also das Verhalten
von LWP::UserAgent
.
Anders im Fall eines übergebenen Strings: Hier gibt ref $req
einen
Leerstring zurück: Um den XML-Salat zu parsen,
kommt das Modul XML::Parser
zum Einsatz. Es handelt
sich nicht um einen prüfenden Parser, der zunächst die Übereinstimmung
mit einer gegebenen DTD (Syntaxbeschreibung des Markups) prüfen würde,
sondern um ein einfaches Tool, das lediglich die ``Wohlgeformtheit''
des XML-Codes sicherstellt, also unter anderem nachprüft, ob alle
geöffneten Tags wieder geschlossen wurden und keine unerlaubten
Sonderzeichen vorkommen. Wer den Markup hingegen auf Syntaxfehler gegenüber
der DTD abklopfen will, muss sich James Clarks Parser sp
besorgen ([3]),
der gibt dann Dinge wie
nsgmls:test.xml:2:33:E: required attribute "url" not specified
aus, falls ein erforderliches Attribut fehlt und man den Dokumenttyp im XML-Markup mit
<!DOCTYPE request SYSTEM "request.dtd">
angegeben hat.
Nein, heute soll's bescheiden zugehen.
Zeile 48 erzeugt ein neues XML::Parser
-Objekt, welches
als Callback für sich öffnende Tags die Routine start_handler
im gleichen Modul festlegt.
Normalerweise arbeitet XML::Parser
gleich mit drei Callbacks:
Start
, End
und Char
definieren Handler, die aufgerufen
werden, falls sich öffnende oder schließende Tags oder dazwischenliegender
Text zeigen. Die erste Version des Moduls wurde übrigens von Larry Wall
entwickelt.
Da unser XML-Markup eigentlich nur leere Elemente mit Attributen
definiert, haben die Char
und End
-Handler keinen Einfluß auf
das Ergebnis und können weggelassen werden. Außerdem verzichtet
XmlUserAgent
auf jegliche Konsistenzprüfungen des Markups,
um das Modul einfach zu halten.
Zeile 50 startet den Parser, der sich durch das übergebene
XML-Dokument frisst, für jeden Start-Tag die
Funktion handle_start
aufruft und ihr eine Referenz
auf das XmlUserAgent
-Objekt ($a
), sowie
(das erledigt XML::Parser
intern)
eine Parserreferenz ($p
), den Namen des Elements ($el
)
und einen Hash mit Attributwerten (%atts
) übergibt.
handle_start
analysiert dann, welcher Art das Element ist,
fieselt die entsprechenden Attribute hervor und speichert
sie als Instanzvariablen des XmlUserAgent
-Objekts ab.
Im Falle von parameter
oder header
-Konstrukten
werden die gefundenen Key/Value-Paare an
die bereitgestellten Listen angehängt.
Sinn und Zweck der Übung: Ist der Text geparst, liegen alle
notwendigen Informationen über den bevorstehenden Request in
Instanzvariablen des XmlUserAgent
-Objekts und können bei
Bedarf hervorgezogen werden. So prüft Zeile 52 ob ein POST
-Request
verlangt wurde, und, falls ja, ruft sie die von
HTTP::Request::Common
praktischerweise exportierte POST
-Funktion
auf, die aus einem URL, einer Referenz auf eine Parameterliste und
einer Headerliste ein HTTP::Request
-Objekt zimmert.
Für den Fall, dass nach einem GET
-Request verlangt wird,
muß in Zeile 57 zunächst die query_form
-Methode der URI::URL
-Klasse
die Parameter an den URL anhängen, bevor in Zeile 58 die ebenfalls
von HTTP::Request::Common
exportierte GET
-Funktion daraus
ein HTTP::Request
-Objekt baut. Zeile 62 gibt das
aus dem XML-String erzeugte HTTP::Request
-Objekt dann
lediglich an die request
-Methode
der Basisklasse weiter, die dann den HTTP-Request
tatsächlich durchführt.
get_basic_credentials
ab Zeile 30 ist die Funktion, die der
LWP::UserAgent
(oder abgeleitete Klassen wie unser
XmlUserAgent
) aufrufen, wenn sie auf ein Webdokument
stoßen, das Benutzernamen und Passwort zur Authorisierung
verlangt. get_basic_credentials
soll Benutzernamen und Passwort
als Liste zurückgeben, falls sie weiß, wie man sich einloggt,
und undef
, falls nicht, was in den Zeilen 34 und 35
passiert. Ist die Instanzvariable username
definiert, spezifizierte
das XML-Dokument höchstwahrscheinlich eine credential
-Sektion
mit der UserID und dem Passwort für den Request.
Die Module LWP::UserAgent
, HTTP::Cookies
und XML::Parser
gibt's
wie immer auf dem CPAN (LWP::UserAgent
ist in Bundle::LWP
enthalten),
die praktische CPAN-Shell spart wie immer Zeit beim Installieren.
Damit auch SSL-Requests
nach dem https
-Protokoll verarbeitet werden können, muß
zusätzlich Crypt::SSLeay
installiert sein, das dem Modul beiliegende
Installationsdokument verrät auch, wie man den kostenlosen SSL-Code
bekommt.
Als kleines Testbeispiel zeigt Listing amazon.xml
die Definition
eines Requests, der in das Suchfeld auf amazon.com das Wort
"Schilli"
eintippt und die ``Go''-Taste drückt. Der Aufruf
fetch.pl amazon.xml
gibt das von Amazon auf die Anfrage zurückgesandte HTML über die
Standardausgabe aus. Die Felder für die Definition in amazon.xml
kann man entweder von Hand aus der Amazon-Seite extrahieren oder
das schon erwähnte Tool von Randal Schwartz ([1]) verwenden.
index
ist die Auswahlbox, die die Suche auf einen der verschiedenen
Amazon-Geschäftsbereiche beschränkt, im vorliegenden Fall
werden mit books
Bücher ausgewählt, und field-keywords
ist
der Name des Suchfeldes, in das der eingegebene Text kommt.
Hidden Fields hat das Formular keine, falls doch, kämen
diese auch in eine <parameter>
-Sektion der XML-Beschreibung.
Der zusätzlich definierte Referer
-Header imitiert den Browser, der
auch die Formularseite dort ablegen würde.
In der nächsten Folge zaubern wir eine kleine Test-Suite, die unsere Web-Formulare abklappert und überprüft, ob noch alles funktioniert -- gerne auch mit Cookies und allem Schnickschnack!
Wer übrigens denkt, ich hätte zuviel objektorientierte Medizin geschluckt, hat ganz recht: Ich las [4] und war begeistert. Ein klasse Buch! Sofort lesen! Bis nächsten Monat!
Abb.1: Die Antwort des Dump-Skripts auf den Request |
<request url="http://localhost" />
<request url="http://localhost/cgi-bin/dump.cgi" method="POST"> <parameter name="first_name" value="Michael" /> <parameter name="last_name" value="Schilli" /> <header name="X-Header1" value="header1" /> <header name="X-Header2" value="header2" /> </request>
<request url="http://localhost/geheim/index.html" method="GET" proxy="http://localhost:3128"> <credentials username = "michael" password = "nixgibts!" /> </request>
<!ELEMENT request (parameter|header|credentials)*> <!ELEMENT parameter EMPTY> <!ELEMENT header EMPTY> <!ELEMENT credentials EMPTY> <!ATTLIST request url CDATA #REQUIRED method CDATA #IMPLIED proxy CDATA #IMPLIED> <!ATTLIST parameter name CDATA #REQUIRED value CDATA #REQUIRED> <!ATTLIST header name CDATA #REQUIRED value CDATA #REQUIRED> <!ATTLIST credentials username CDATA #REQUIRED password CDATA #REQUIRED>
01 #!/usr/bin/perl -w 02 03 use XmlUserAgent; 04 use HTTP::Cookies; 05 06 $ua = XmlUserAgent->new(); 07 $jar = HTTP::Cookies->new(); 08 09 $ua->cookie_jar($jar); 10 11 foreach $xml (@ARGV) { 12 open FILE, "<$xml" or die "Cannot open $xml"; 13 my $data = join '', <FILE>; 14 close FILE; 15 16 $response = $ua->request($data); 17 18 { # Handle POST redirects 19 if($response->code == 302) { 20 $request = HTTP::Request->new(GET => 21 $response->header('Location')); 22 $response = $ua->request($request); 23 redo; 24 } 25 } 26 } 27 28 if ($response->is_success) { 29 print $response->headers_as_string(), "\n", 30 $response->content(), "\n\n"; 31 } else { 32 print "Error! Code=", $response->code, "\n"; 33 print "Message=", $response->message, "\n\n"; 34 }
01 ################################################## 02 package XmlUserAgent; 03 ################################################## 04 05 use LWP::UserAgent; 06 use URI::URL; 07 use XML::Parser; 08 use HTTP::Request::Common; 09 10 @ISA = qw(LWP::UserAgent); 11 12 ################################################## 13 sub new { # Constructor 14 ################################################## 15 my ($class) = @_; 16 17 my $self = { ua => LWP::UserAgent->new(), 18 url => undef, 19 method => undef, 20 headers => [], 21 params => [], 22 username => undef, 23 password => undef, 24 }; 25 26 bless $self, $class; 27 } 28 29 ################################################## 30 sub get_basic_credentials { 31 ################################################## 32 my $self = shift; 33 34 return undef unless exists $self->{username}; 35 return($self->{username}, $self->{password}); 36 } 37 38 ################################################## 39 sub request { 40 ################################################## 41 my ($self, $req) = @_; 42 43 return shift->{ua}->request(@_) if ref $req; 44 45 my $start_handler = sub { 46 handle_start($self, @_) }; 47 48 my $p = XML::Parser->new( 49 Handlers => { Start => $start_handler }); 50 $p->parse($req); 51 52 if($self->{method} eq "POST") { 53 $req = POST($self->{url}, $self->{params}, 54 @{$self->{headers}}); 55 } else { 56 # GET request -- assemble parameters 57 $self->{url}->query_form(@{$self->{params}}); 58 $req = GET($self->{url}, 59 @{$self->{headers}}); 60 } 61 62 return $self->{ua}->request($req); 63 } 64 65 ################################################## 66 sub handle_start { 67 ################################################## 68 my ($a, $p, $el, %atts) = @_; 69 70 if($el eq "request") { 71 $a->{method} = $atts{method} || "GET"; 72 $a->{url} = URI::URL->new($atts{url}); 73 $a->{ua}->proxy($a->{url}->scheme, $atts{proxy}) if 74 exists $atts{proxy}; 75 } 76 77 if($el eq "credentials") { 78 $a->{username} = $atts{username}; 79 $a->{password} = $atts{password}; 80 } 81 82 if($el eq "parameter") { 83 push(@{$a->{params}}, 84 $atts{name}, $atts{value}); 85 } 86 87 if($el eq "header") { 88 push(@{$a->{headers}}, 89 $atts{name}, $atts{value}); 90 } 91 } 92 93 1;
01 <request url="http://www.amazon.com/exec/obidos/search-handle-form" 02 method="POST"> 03 04 <parameter name="index" value="books" /> 05 <parameter name="field-keywords" value="schilli" /> 06 <parameter name="Go" value="Go" /> 07 08 <header name="Referer" 09 value="http://www.amazon.com/exec/obidos/subst/home/home.html" /> 10 11 </request>
sp
: http://www.jclark.com/sp/xml.htm
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. |