Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / Perl

Perl web automation

Abstract
Per molti Perl è stato ed è il linguaggio per i CGI o uno dei tools indispensabili per analizzare i log di un server.
In questo articolo verrà presentato quello che si può fare con Perl non solo dentro o dietro le quinte di un web server.
Data di stesura: 08/02/2005
Data di pubblicazione: 17/02/2005
Ultima modifica: 04/04/2006
di Marco Lamberto Discuti sul forum   Stampa

Web automation

Simulare l'interazione di un utente mediante un browser è un'esigenza che nasce in diversi ambiti per trovare una collocazione quanto mai naturale all'interno delle attività di testing.

In questo documento verranno presentate una serie di moduli per il linguaggio Perl, liberamente disponibili, che possono essere usati nell'ottica della web automation.

Un esempio concreto

Prendiamo ad esempio la pagina principale di un sito sicuramente noto ai più: freshmeat.net. Il sito permette agli utenti registrati di vedere le notizie e le releases in maniera selettiva.

Supponiamo di voler realizzare un micro agente in Perl che operi una login sul sito, acceda alla sezione degli articoli e stampi a video i primi dieci titoli con i relativi links.

Nella figura successiva potete vederne una porzione, è stato evidenziato in rosso il form di login.

[Figura 1]

Figura 1: La pagina principale di freshmeat.net

Gli utenti dei browser Mozilla, Netscape e Firefox, possono accedere a delle informazioni dettagliate sulla pagina tramite la funzione di "View Page Info". Di particolare utilità è il tab "Forms".

Nella pagina principale del sito sono presenti quattro forms, essendo tutti anonimi, un breve confronto con la pagina relativa permette di scoprire che il quarto è quello che c'interessa.

[Figura 2]

Figura 2

Dei campi contenuti "username" e "password" sono gli unici che verranno usati.
È importante non dimenticare il campo "Form Action", perché è la pagina a cui devono essere mandati i dati della form mediante una richiesta con metodo POST.

L'algoritmica della soluzione all'esempio proposto, visti i fini didattici, è molto semplice, nulla vieta di estenderla per permettere il salvataggio dei cookies tra un'esecuzione e l'altra e sfruttare il meccanismo di login persistente fornito da freshmeat.net.

Un primo approccio

Inizialmente si potrebbe pensare di ricorrere all'uso del modulo Socket o meglio ancora di IO::Socket, quest'ultimo infatti fornisce un'interfaccia ad oggetti per l'uso delle socket.

L'esempio successivo mostra un uso di IO::Socket per ottenere la pagina all'indirizzo http://localhost/.

  1. #!/usr/bin/perl -w 
  2. use IO::Socket; 
  3.  
  4. $remote = IO::Socket::INET->new( 
  5.     Proto    => "tcp", 
  6.     PeerAddr => "localhost", 
  7.     PeerPort => "http(80)", 
  8.   ) or die "cannot connect to http port at localhost"; 
  9.  
  10. print $remote <<'EOT'; 
  11. GET / HTTP/1.0 
  12. Host: localhost 
  13.  
  14.  
  15. EOT 
  16.  
  17. while (<$remote>) { 
  18.   print; 

Risulta più che mai evidente lo svantaggio maggiore di un approccio eccessivamente a basso livello che porta alla gestione manuale non solo della connessione TCP/IP ma anche di tutto il protocollo HTTP, senza dimenticare la gestione degli errori per entrambi i protocolli (con conseguenti deliri e follia).

La situazione con Socket è peggiore, poiché questo modulo non fa altro che esporre in Perl le primitive C di socket.h, tanto vale scrivere il tutto direttamente in C!

Ovviamente lo scopo primario per uno sviluppatore dovrebbe essere semplificarsi la vita per concentrarsi sul problema e non sui problemi necessari per ottenere un contesto risolutivo del problema ... anche se non sempre è così.
Per fortuna in questo caso ne possiamo uscire con tutti i capelli attaccati alla testa! (E non perché abbiamo strappato quelli del nostro compagno di "pair programming"!)

LWP::UserAgent

Dimentichiamoci delle socket TCP e passiamo poco sopra ad HTTP: esiste da parecchi anni libwww-perl [4], una collezione di moduli che permettono la programmazione in maniera agevole e completa di web clients.

Nello specifico il modulo LWP::UserAgent permette di simulare un client web.
Questo fornisce il supporto per richieste GET, POST e HEAD, redirezioni, proxy, cookies e molto altro, il tutto, ovviamente, con un approccio ad oggetti.

  1. require LWP::UserAgent; 
  2.  
  3. my $ua  = LWP::UserAgent->new; 
  4.  
  5. $ua->env_proxy; 
  6.  
  7. my $res = $ua->get('http://localhost/'); 
  8.  
  9. die $res->status_line unless $res->is_success; 
  10.  
  11. print $res->content; 

Segnalo il modulo LWP::Simple che espone un'interfaccia procedurale equivalente a LWP::UserAgent.

Ecco dunque cosa accade quando si usa questo modulo per tentare di risolvere il nostro problema:

  1. #!/usr/bin/perl -w 
  2.  
  3. use strict; 
  4. use LWP::UserAgent; 
  5. use HTTP::Cookies; 
  6. use IO::Handle; 
  7.  
  8. autoflush STDERR 1; 
  9.  
  10. # LWP::UserAgent setup 
  11.  
  12. my $cookies = HTTP::Cookies->new; 
  13. my $ua      = LWP::UserAgent->new( 
  14.   keep_alive              => 1, 
  15.   agent                   => 'SIBot/1.0', 
  16.   requests_redirectable   => ['HEAD', 'GET', 'POST'], 
  17.   cookie_jar              => $cookies, 
  18. ); 
  19.  
  20. $ua->env_proxy; 
  21.  
  22.  
  23. # quick'n'dirty user/pass request 
  24.  
  25. print STDERR "Login: "; 
  26. my $user = <STDIN>; 
  27. chomp $user; 
  28.  
  29. print STDERR "Password: "; 
  30. my $pass = <STDIN>; 
  31. chomp $pass; 
  32.  
  33.  
  34. # login 
  35.  
  36. my $login = $ua->post('http://freshmeat.net/login', { 
  37.   url         => '/', 
  38.   username    => $user, 
  39.   password    => $pass, 
  40.   persistent  => 1, 
  41.   submit      => 'Login', 
  42. }); 
  43.  
  44. die "login failed" unless 
  45.   $login->is_success or $login->content =~ /logged in as user /; 
  46.  
  47.  
  48. # retrieve data 
  49.  
  50. my $articles  = $ua->get('http://freshmeat.net/lounge/articles/'); 
  51.  
  52. die "failed retrieving articles" unless $articles->is_success; 
  53.  
  54.  
  55. # content parsing 
  56.  
  57. my @list = grep(m#<a href="[^"]+"><b>[^<]+</b></a>#, 
  58.   split(/[\r\n]/, $articles->content)); 
  59.  
  60. foreach (@list[0 .. 9]) { 
  61.   m#<a href="([^"]+)"><b>([^<]+)</b></a>#; 
  62.   print "$2\nhttp://freshmeat.net$1\n"; 

Confrontando l'esempio iniziale di LWP::UserAgent con quest'ultimo esempio, è possibile notare come la complessità del sorgente sia ovviamente aumentata, in particolare si rende necessario tenere traccia di una serie di riferimenti ad oggetti creati lungo il flusso d'esecuzione.
Quest'ultima esigenza non è un grosso problema, tuttavia una serie di tare legate allo sviluppo con LWP::UserAgent sono superabili grazie all'adozione di un altro modulo.

WWW::Mechanize

WWW::Mechanize[5] è un'estensione di LWP::UserAgent e rende decisamente più agile la realizzazione di scripts di automazione in virtù di una serie di metodi che permettono di navigare con continuità attraverso i links ed i documenti html scaricati.

Una delle prime cose da notare è che WWW::Mechanize tenta di emulare un web client "classico", l'interfaccia esposta è sicuramente più ad alto livello di LWP::UserAgent, poiché ad esempio tiene traccia della sequenza di link visitati in maniera analoga alla history di un browser, gestisce i cookies in maniera automatica (il default di LWP::UserAgent è di non gestirli, da qui la necessità nell'esempio precedente di passargli un cookie_jar).

Altro punto a favore di WWW::Mechanize è la possibilità di compilare i forms e farne il submit più comodamente di quanto non si riesca con LWP::UserAgent.

  1. #!/usr/bin/perl -w 
  2.  
  3. use strict; 
  4. use WWW::Mechanize; 
  5. use IO::Handle; 
  6.  
  7. autoflush STDERR 1; 
  8.  
  9. # WWW::Mechanize setup 
  10.  
  11. my $mech    = WWW::Mechanize->new( 
  12.   keep_alive              => 1, 
  13.   agent                   => 'SIBot/1.1', 
  14.   autocheck               => 1, 
  15. ); 
  16.  
  17. $mech->env_proxy; 
  18.  
  19.  
  20. # quick'n'dirty user/pass request 
  21.  
  22. print STDERR "Login: "; 
  23. my $user = <STDIN>; 
  24. chomp $user; 
  25.  
  26. print STDERR "Password: "; 
  27. my $pass = <STDIN>; 
  28. chomp $pass; 
  29.  
  30.  
  31. # login 
  32.  
  33. $mech->get('http://freshmeat.net/'); 
  34. $mech->submit_form( 
  35.   form_number => 4, 
  36.   fields      => { 
  37.     username  => $user, 
  38.     password  => $pass, 
  39. ); 
  40.  
  41. die "login failed" unless $mech->content =~ /logged in as user /; 
  42.  
  43.  
  44. # retrieve data 
  45.  
  46. $mech->follow_link(text_regex => qr/\[My\]/) or die; 
  47. $mech->follow_link(text_regex => qr/\d+ new articles/) or die; 
  48.  
  49.  
  50. # content parsing 
  51.  
  52. my @list = grep(m#<a href="[^"]+"><b>[^<]+</b></a>#, 
  53.   split(/[\r\n]/, $mech->content)); 
  54.  
  55. foreach (@list[0 .. 9]) { 
  56.   m#<a href="([^"]+)"><b>([^<]+)</b></a>#; 
  57.   print "$2\nhttp://freshmeat.net$1\n"; 

Rispetto al precedente esempio cambiano una serie di cose, la prima che spicca è l'assenza di variabili temporanee per le operazioni di "navigazione", WWW::Mechanize mantiene tutto al suo interno ed è possibile interrogarlo solo quando servono realmente i dati.

Altra cosa importante è la gestione automatica degli errori di connessione tramite l'impostazione ad 1 del valore di autocheck.
Così facendo non è più necessario valutare ad ogni operazione se è andata a buon fine o meno.
La gestione degli errori si può quindi spostare un po' più in alto e limitarsi a controllare che la pagina ottenuta sia quella voluta:

  1. die "login failed" unless 
  2.   $login->is_success or $login->content =~ /logged in as user /; 
  1. die "login failed" unless $mech->content =~ /logged in as user /; 

L'altra cosa che ho cercato di rendere evidente è l'approccio differente alla navigazione che si ha con WWW:Mechanize, è possibile seguire i links senza dover inserire le url di cui fare GET o POST.

  1. $mech->follow_link(text_regex => qr/\[My\]/) or die; 
  2. $mech->follow_link(text_regex => qr/\d+ new articles/) or die; 

Altrettanto utile è la possibilità di fare il submit di un form semplicemente specificare solo i campi che si vogliono modificare o riempire, per gli altri userà il valore di default specificato nel documento HTML.

Installazione dei moduli Perl mancanti

Non tutte le distribuzioni di Perl includono i moduli presentati.
L'installazione, anche da utente semplice, può essere fatta appoggiandosi al modulo CPAN.

$ perl -MCPAN -e shell
Terminal does not support AddHistory.

cpan shell -- CPAN exploration and modules installation (v1.7601)
ReadLine support available (try 'install Bundle::CPAN')

cpan>

A questo punto basterà scrivere il comando "install WWW::Mechanize" (e confermarlo con il tasto di invio!) per procedere con l'installazione del modulo e delle sue eventuali dipendenze mancanti.

Per ulteriori dettagli suggerisco la lettura della pagina di manuale relativa al modulo CPAN mediante il comando "perldoc CPAN".

Conclusioni

Come accennato all'inizio del documento, l'automazione è molto utile durante il web testing. A partire dalla generazione dei singoli test fino alla loro esecuzione, sono problematiche decisamente pressanti. In molti casi ricorrere a soluzioni script-based può costituire un risparmio notevole in tempo e grattacapi.
Nel prossimo articolo affronteremo la problematica dell'automazione del testing (registrazione ed esecuzione) con Perl grazie all'ausilio di WWW::Mechanize ed un paio di moduli di supporto.

Informazioni sull'autore

Marco Lamberto, laureato in Informatica presso la Statale di Milano, con diversi anni di esperienza sistemistica, di sicurezza e sviluppo prevalentemente in ambienti UNIX (Linux in primis) con linguaggi come C, Java, Perl, PHP, XML, HTML, ...

È possibile consultare l'elenco degli articoli scritti da Marco Lamberto.

Altri articoli sul tema Linguaggi / Perl.

Risorse

  1. Sorgenti del primo esempio con LWP::UserAgent.
    http://www.siforge.org/articles/2005/02/perl_web_automation/fm_lwp.pl (2Kb)
  2. Sorgenti del secondo esempio con WWW::Mechanize.
    http://www.siforge.org/articles/2005/02/perl_web_automation/fm_mec.pl (2Kb)
  3. Sito ufficiale del progetto libwww-perl.
    http://www.linpro.no/lwp/
  4. Pagina del CPAN relativa al pacchetto di libwww-perl.
    http://search.cpan.org/~gaas/libwww-perl/
  5. Pagina del CPAN relativa al pacchetto di WWW::Mechanize.
    http://search.cpan.org/~petdance/WWW-Mechanize/
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.