Article publié dans Linux Magazine 58, février 2004.
Copyright © 2004 - Philippe Bruhat.
Cette dernière partie est presque tout entière consacrée à WWW::Mechanize. Comme vous allez le constater, ce module simplifie encore la création de scripts pour la navigation web.
Malgré son respect des RFC, sa modularité et son exhaustivité, LWP::UserAgent reste un module complexe, qui nécessite de connaître un minimum de choses sur le protocole HTTP (et parfois sur le fonctionnement interne de LWP). Dans un script d'automatisation, on ne cherche pas tant à utiliser HTTP, qu'à seulement récupérer une page web et éventuellement construire une nouvelle requête à partir d'élements de cette page.
Il existe plusieurs modules qui encapsulent les interfaces de LWP::UserAgent dans une interface simplifiée ou dédiée à l'automatisation.
Nous avons déjà vu LWP::Simple dans le premier article de cette série.
Il est souvent assez fastidieux de créer un script complet avec LWP. C'est un peu toujours la même chose. N'oubliez donc pas l'existence de LWP::Simple, qui fournit des interfaces très simples. Il vous évite en particulier de créer l'objet HTTP::Request et de décortiquer l'objet HTTP::Response.
En revanche, on est limité aux méthodes GET et HEAD. Et il n'y a pas de moyen de traiter complètement les erreurs. Cet inconvénient est contre-balancé par la rapidité de cette librairie pour certaines opérations.
Gisle Aas a en effet optimisé la méthode get()
de LWP::Simple
pour qu'elle soit aussi rapide que possible pour les cas simples.
Du coup, il est plus performant d'utiliser celle-ci dans un
script plutôt que LWP::UserAgent pour faire la même chose.
C'est d'ailleurs ce que nous avons constaté à la fin du premier article
de cette série, dans Linux Magazine 56.
En général, LWP::Simple permet de récupérer de l'information sur une page et la traiter directement ensuite.
WWW::Mechanize (surnommé Mech) est en fait une classe qui hérite de LWP::UserAgent. Ce module est activement développé par Andy 'petdance' Lester sur sourceforge. À l'origine, WWW::Mechanize est la reprise de WWW::Automate, de Kirrily 'Skud' Roberts, module s'inspirant lui-même de WWW::Chat, un système de macros écrit initialement par Gisle Aas et lui-même repris ensuite par Simon Wistow. WWW::Chat nécessite un préprocesseur (webchatpp), tandis que WWW::Automate (qui n'est plus maintenu) et WWW::Mechanize sont des modules qui offrent une interface orientée objet.
WWW::Mechanize, comme son nom l'indique, a pour but principal d'écrire des robots (ou automates) pour la navigation sur le web.
Vous pouvez vous reporter au premier article de cette série (Linux Mag 56) pour vous remettre en mémoire le script perlmonks utilisant LWP::UserAgent. Voici le même exemple, écrit cette fois avec WWW::Mechanize :
#!/usr/bin/perl -w use strict; use WWW::Mechanize; use HTTP::Cookies; use Getopt::Std; my $base = 'http://www.perlmonks.org/'; my %conf = ( f => "$ENV{HOME}/.perlmonksrc" ); # -l login:pass # -f config_file getopts( 'l:f:', \%conf ) or die "Bad parameters"; # initialisation de l'agent my $bot = WWW::Mechanize->new( agent => 'Mozilla/4.73 [en] (X11; I; Linux 2.2.16 i686; Nav)', cookie_jar => HTTP::Cookies->new( file => $conf{f}, autosave => 1, ignore_discard => 1, # le cookie devrait être effacé à la fin ) ); # nouvel utilisateur ou pas ? if ( $conf{l} ) { my ( $user, $pass ) = split ':', $conf{l}, 2; # exécute la requête et reçoit la réponse $bot->get("${base}index.pl?node=login"); die $bot->res->status_line if not $bot->success; # sélectionne le formulaire de login (second formulaire de la page) $bot->form_number(2); # remplit les champs $bot->set_fields( user => $user, passwd => $pass ); # valide le formulaire $bot->submit; } # sinon on établit la connexion (avec le cookie s'il existe) else { $bot->get("${base}index.pl?node=login"); }
Les différences sont assez minimes : comme WWW::Mechanize dérive de LWP::UserAgent, on l'initialise de la même façon, en lui passant un objet HTTP::Cookies (cookie jar) si nécessaire.
L'intérêt principal de WWW::Mechanize réside dans sa particularité : un agent WWW::Mechanize conserve et prépare tout un contexte autour des URI et formulaires visités. Ces informations, couplées avec des méthodes d'accès simplifiées, prennent tout leur intérêt quand on veut réaliser une session complète constituée d'un ensemble de requêtes web liées entre elles : tout d'abord il n'y a pas à préparer les requêtes (il suffit d'indiquer l'URI de départ) et ensuite il suffit de sélectionner les formulaires, mettre à jour les champs et valider. On répète ainsi l'opération autant de fois qu'il est nécessaire.
Le project WWW::Mechanize est hébergé sur SourceForge
(http://sourceforge.net/projects/www-mechanize/) et deux listes
de diffusion existent : www-mechanize-cvs
, qui envoie un courriel
automatique pour chaque commit dans le CVS et www-mechanize-development
où les développeurs du module font part de leurs projets et discutent
des évolutions possibles du module. Le trafic est peu volumineux.
Pour vous abonner, consultez la page du projet sur SourceForge.
WWW::Mechanize est activement développé : 9 nouvelles versions sont sorties ces trois derniers mois. La version de WWW::Mechanize utilisée pour cet article est la 0.70, qui requiert également la toute dernière version de LWP, la 5.76.
Comme pour tout module orienté objet, on commence par créer une instance de la classe, c'est-à-dire un objet WWW::Mechanize.
my $bot = WWW::Mechanize->new();
Puisque WWW::Mechanize dérive de LWP::UserAgent, il accepte les arguments du constructeur de LWP::UserAgent, en plus de ses propres paramètres d'initialisation (qui concernent principalement les avertissements et erreurs.
Notez que WWW::Mechanize utilise toujours les proxies définis par les variables d'environnement. Par défaut, il stocke également les cookies en mémoire. Si vous ne voulez pas que votre robot accepte les cookies, vous pouvez l'initialiser comme suit :
WWW::Mechanize->new( cookie_jar => undef );
Si au contraire vous voulez qu'il stocke les cookies dans un fichier, il vous faudra alors créer votre propre objet HTTP::Cookies et lui passer, comme nous l'avons fait dans l'exemple précédent sur Perlmonks.
Une fois votre robot créé, vous pouvez commencer à surfer :
my $response = $bot->get( 'http://www.example.com/' );
La méthode get()
retourne un objet HTTP::Response. Vous n'aurez en
fait que rarement besoin de l'utiliser, puisqu'il est également stocké
par WWW::Mechanize. get()
reproduit le fonctionnement de la méthode
get()
de LWP::UserAgent et accepte les mêmes paramètres.
Comme je n'ai pas parlé de la méthode get()
de LWP::UserAgent dans les
articles précédents, je vais rapidement vous la présenter. Le premier
paramètre est une URI. Les suivants permettent de mettre à jour simplement
les en-têtes de la requête, passés comme d'habitude comme des couples
clé/valeur :
$ua->get( "http://www.example.com", Referer => "http://www.mongueurs.net/");
Certains paramètres spéciaux, précédés par des :
ne sont pas des en-têtes,
mais des paramètres pour la méthode request()
de LWP::UserAgent. Nous
n'avons pas non plus vu ces paramètres dans les articles précédents.
Pour résumer, la méthode request()
de LWP::UserAgent accepte comme
paramètres un nom de fichier (pour sauvegarder le corps de la réponse)
ou une référence à une routine (callback) qui sera appelée au fur et
à mesure de la réception de la réponse. Le paramètre read_size_hint
sert à donner à LWP::UserAgent un indice concernant la taille des morceaux
de réponse attendus (ce paramètre n'est qu'indicatif).
En bref, ces paramètres sont :
:content_file => $filename :content_cb => \&callback :read_size_hint => $bytes
Mais revenons à WWW::Mechanize. get()
est en général la première
vraie commande d'un script d'automatisation. Ensuite, la session de
navigation consiste principalement à suivre des liens et remplir des
formulaires.
La méthode follow_link()
permet suivre un lien. Elle accepte plusieurs
types de critères pour séléctionner le lien à suivre :
Les critères text
, url
, name
et tag
permettent d'obtenir les
liens dont le texte, l'URL, le nom ou la balise HTML sont égaux aux
paramètres associés.
Exemple :
# suit le premier lien dont le texte est Download $bot->follow_link( text => 'Download' );
Les critères text_url
, url_regex
, name_regex
et tag_regex
sélectionnent les liens qui correspondent à l'expression rationnelle
fournie en paramètre. Il faut utiliser qr//
pour produire l'expression
rationnelle.
Exemple :
$bot->follow_link( text_regex => qr/mongu?eu?r/, url_regex => qr/paris/ );
Le critère n
permet de choisir le nième lien correspondant.
Si n
n'est pas fourni, c'est le premier lien correspondant qui est
suivi.
Exemples :
# suit le 3ème lien $bot->follow_link( n => 3 ); # suit le second lien contenant "download" dans l'URL $bot->follow_link( url_regex => qr/download/, n => 2 );
Si plusieurs critères sont fournis, seuls les liens qui correspondent à
tous les critères seront sélectionnés. n
permettra de garder le
nième parmi ceux-ci.
La plupart des pages web contiennent plusieurs formulaires. Avant de
commencer à remplir le formulaire choisi, il faut donc le sélectionner.
Ceci se fait à l'aide des méthodes form_number()
et form_name()
pour sélectionner un formulaire en fonction de son numéro d'ordre (indexé
à partir de 1) ou de son nom.
Pour le remplissage du formulaire, la méthode field()
est utilisée :
$bot->form_number(2); $bot->field( 'user', 'BooK' ); # ou en utilisant les propriétés de => $bot->field( passwd => 's3kr3t' );
Si plusieurs champs portent le même nom (par exemple un champ de type
option
), un troisième champ est utilisé pour donner le numéro d'ordre
de ce champ (à partir de 1).
La méthode set_fields()
permet de remplir plusieurs champs d'un coup :
$bot->form_number(2); $bot->set_fields( user => 'BooK', passwd => 's3kr3t' );
Si plusieurs champs du même nom existent, il faudra alors passer un tableau anonyme contenant la valeur et le numéro d'ordre du champ. Pour notre exemple, nous utilisont le formulaire du premier article de cette série :
$bot->set_fields( FRUITS => [ 'poire', 2 ], FRUITS => [ 'banane', 3 ] );
Enfin, la méthode tick()
et untick()
permettent de traiter le cas
des cases à cocher :
$bot->tick( 'RGB', 'rouge' ); $bot->untick( RGB => 'vert' );
L'envoi du formulaire se fait avec les méthodes click()
ou submit()
.
click()
est appelée avec le nom du bouton à cliquer, tandis que
submit()
valide le formulaire sans cliquer sur aucun bouton.
La méthode submit_form()
permet de combiner la sélection, le remplissage
et l'envoi du formulaire en une seule méthode (consultez la documentation
pour plus de détails).
Enfin, il existe un certain nombre de méthodes pour obtenir des informations à
partir de la réponse à la requête HTTP : response()
, content()
,
links()
, etc. Là encore, la documentation vous en donnera la liste
complète. Sachez enfin que comme WWW::Mechanize dérive de LWP::UserAgent,
toutes les méthodes de LWP::UserAgent sont également disponibles.
Andy Lester est un grand fan des tests. C'est pourquoi il utilise Test::More et WWW::Mechanize pour tester le comportement de ses sites web.
use Test::More; like( $bot->content(), qr/$expected/, "" );
Un script de test élaboré va rejouer complètement une session utilisateur, en vérifiant à chaque étape le contenu de la page renvoyée.
Puisque nous parlons d'exemples utiles, je vous propose de réaliser comme le mois dernier un script d'automatisation utilisant WWW::Mechanize en suivant pas à pas les étapes de sa création.
Mailman (http://www.list.org/) est un outil de gestion de listes de diffusion dont l'interface d'administration est accessible par le web. En particulier, les courriels envoyés depuis des adresses non inscrites à la liste sont bloqués dans une file d'attente afin d'être traités manuellement par un administrateur. Il y a donc un formulaire web qui propose les choix suivants : Defer (attendre), Approve (accepter), Reject (rejeter) et Discard (détruire).
Je suis co-administrateur d'une liste gérée par Mailman ; la fois où j'ai vu le formulaire m'annoncer 39 mails en attente, j'ai craqué. Rien que de penser que j'allais devoir tirer sur l'ascenseur, cliquer sur Discard, passer au suivant, recommencer, je n'ai plus eu qu'une idée en tête, automatiser tout. Ne plus jamais utiliser Mozilla pour administrer cette liste.
Le site d'administration de Mailman se trouve en général au bout d'une URI de la forme http://www.example.com/mailmain/admin. De là vous pouvez choisir la liste à administrer. Ensuite, dans Other Administrative Activities / Tend to pending moderator requests, vous trouverez l'URI qui donne accès au formulaire de gestion des courriels en attente.
Ce programme a été écrit avec Mailman 2.0.13, mais avec une petite modification dans l'URL de départ, j'ai pu facilement le rendre compatible avec la version 2.1.3, qui a été installée récemment sur le serveur de ma liste de distribution.
Pour la version 2.0.13, la page donnant les messages bloqués était http://www.example.com/mailman/admindb/maliste. Pour obtenir le même formulaire avec la version 2.1.3, il faut demander la page http://www.example.com/mailman/admindb/maliste?details=all. Selon la version de Mailman utilisée, il faudra donc sélectionner la bonne URL de départ.
L'accès à la page d'admin commence par un formulaire d'authentification. Nous allons donc charger la page, trouver le formulaire, le remplir et le valider.
#!/usr/bin/perl -w use strict; use WWW::Mechanize; use Data::Dumper; my $url = 'http://www.example.com/mailman/admindb/maliste?details=all'; my $pwd = 's3kr3t'; my $bot = WWW::Mechanize->new; $bot->get($url); $bot->form_number(1); print Dumper $bot->current_form; # print Dumper +( $bot->forms )[0]; # autre formulation
WWW::Mechanize fournit des interfaces de haut niveau. Plus besoin
de créer la requête à partir d'une URL, un simple get()
suffit.
La méthode form_number()
permet de sélectionner le formulaire qui sera
le formulaire courant, c'est-à-dire celui sur lequel agiront les
méthodes d'accès aux champs. forms()
quant à elle, renvoie la liste
des formulaires sur la page. Attention, les formulaires sont indexés à
partir de 1.
Nous affichons ainsi le premier (et seul) formulaire de la page :
$VAR1 = bless( { 'inputs' => [ bless( { 'size' => '30', 'type' => 'password', 'name' => 'adminpw' }, 'HTML::Form::TextInput' ), bless( { 'value' => 'Let me in...', 'type' => 'submit', 'name' => 'request_login' }, 'HTML::Form::SubmitInput' ) ], 'enctype' => 'application/x-www-form-urlencoded', 'method' => 'POST', 'attr' => { 'method' => 'POST' }, 'action' => bless( do{\(my $o = 'http://www.example.com/mailman/admindb/maliste?details=all')}, 'URI::http' ) }, 'HTML::Form' );
Pour les formulaires très gros, il est plus simple d'utiliser mech-dump (voir plus bas la section Outils), qui rend un affichage plus lisible :
$ mech-dump --forms http://www.example.com/mailman/admindb/maliste?details=all POST http://www.example.com/mailman/admindb/maliste adminpw= (password) request_login=Let me in... (submit)
Le champ à remplir avec le mot de passe s'appelle donc adminpw
.
Note : dans les exemples de code qui suivent, je n'indiquerai plus la liste des modules utilisés. C'est seulement quand un nouveau module sera utilisé que je le préciserai.
my $pwd = 's3cr3t'; # page de login principale $bot->get( $url ); $bot->form_number(1); $bot->field( adminpw => $pwd ); $bot->click();
Pour récupérer une référence à un formulaire, il faut utiliser soit
la méthode current_form() (qui renvoie le formulaire actif), soit
aller le chercher directement dans le résultat de forms()
(qui renvoie
la liste de tous les formulaires de la page).
La méthode click() valide le formulaire. Elle prend en paramètre le nom du
bouton sur lequel il faut "cliquer". S'il n'y a qu'un seul bouton dans
le formulaire, il n'est pas nécessaire de donner son nom. Nous aurions
également pu utiliser submit()
.
Une fois qu'on s'est authentifié, on reçoit une page avec un formulaire contenant une description de tous les messages à modérer. Pour se faire une première idée de ces données, on utilise le code suivant :
# la page de spam $bot->form_number(1); print Dumper $bot->current_form;
Comme WWW::Mechanize descend de LWP::UserAgent, il peut comme lui gérer les cookies. Nous ouvrirons donc à notre robot la boîte à cookies :
use HTTP::Cookies; $bot->cookie_jar( HTTP::Cookies->new( file => "$ENV{HOME}/.mailmanrc", autosave => 1, ignore_discard => 1, ) );
Les cookies seront donc sauvegardés automatiquement dans le fichier .mailmanrc dans notre répertoire personnel. Comme dans le cas précédent, le cookie est un cookie de session, qui doit être effacé à la fin de l'exécution du programme.
Relançons encore une fois le script et voyons le résultat dans ~/.mailmanrc :
#LWP-Cookies-1.0 Set-Cookie3: maliste:admin=2832333333693203d90e702833333308020165050609070903660100036665096105000907656464010060050562626107656208000903; path="/mailman/"; domain=www.example.com; path_spec; discard; version=1
Ça marche, nous avons capturé le cookie renvoyé par Mailman.
Nous avons un petit problème, maintenant : puisque nous lisons le cookie dans notre fichier, la première page qui nous sera renvoyée est directement celle contenant le formulaire avec tous les messages à valider. L'étape de login devient inutile, mais reste nécessaire lors du premier lancement du programme.
Ici encore, la solution est simple : si le premier formulaire reçu est le formulaire de login, on le remplit et on valide, sinon on passe directement à la suite. Voici comment coder cela :
# charge le premier formulaire $bot->get($url); $bot->form_number(1); # ne se logge que si nécessaire if ( $bot->current_form->find_input('adminpw') ) { warn "Rechargement du cookie\n"; $bot->field( adminpw => $pwd ); $bot->click(); die "Pas de message en attente\n" unless scalar @{$bot->forms}; $bot->form_number(1); }
La méthode find_input()
de HTML::Form
permet de retrouver un champ
du formulaire par son nom, son type ou son numéro d'ordre. Ici nous
cherchons simplement s'il y a un champ nommé adminpw
. Si oui, c'est
la page de login, si non, nous sommes déjà arrivés sur la page qui nous
intéresse depuis le début.
Une fois chargée la page contenant le formulaire d'administration, il ne nous reste plus qu'à trouver les bonnes cases à cocher et valider le formulaire.
Tout d'abord, voyons un peu le contenu du formulaire :
print $_->name for $bot->current_form->inputs;
Ceci nous affiche les noms des différents champs :
submit 2085 preserve-2085 forward-2085 forward-addr-2085 comment-2085 headers-2085 fulltext-2085 2095 preserve-2095 forward-2095 forward-addr-2095 comment-2095 headers-2095 fulltext-2095 submit
On retrouve donc les deux boutons Submit et un certain nombre de champs par message. Avec Data::Dumper, nous pouvons observer rapidement chaque objet HTML::Form::Input.
Par exemple, le champ 2085
correspond à l'objet suivant :
$VAR1 = bless( { 'seen' => [ 1, 0, 0, 0 ], 'menu' => [ '0', '1', '2', '3' ], 'current' => 0, 'type' => 'radio', 'name' => '2085' }, 'HTML::Form::ListInput' ) );
En regardant le source HTML de la page (qu'on peut obtenir par la
méthode $bot->content
), on découvre rapidement notre
formulaire :
<table CELLPADDING="0" CELLSPACING="5"> <tr> <td> Defer </td> <td> Approve </td> <td> Reject </td> <td> Discard </td> </tr> <tr> <td><center><INPUT name="2085" type="RADIO" value="0" CHECKED ></center></td> <td><center><INPUT name="2085" type="RADIO" value="1" ></center></td> <td><center><INPUT name="2085" type="RADIO" value="2" ></center></td> <td><center><INPUT name="2085" type="RADIO" value="3" ></center></td> </tr> </table>
Et on peut ainsi faire la correspondance :
Defer => 0 Approve => 1 Reject => 2 Discard => 3
Le code de suppression des messages est alors rapide à écrire.
# traitement des différents champs du formulaire my $msg = 0; for ( $bot->current_form->inputs ) { # affiche certains en-têtes if ( $_->name =~ /^headers-(\d+)$/ ) { print "$1 ", join "$1 ", $_->value =~ m!^((?:From|Subject):.*?\n)!img; print $/; next; } # coche l'effacement des messages $_->value(3), $msg++, next if $_->name =~ /^\d+$/; } # valide le formulaire $bot->click;
Le script affiche de plus le sujet et l'auteur du message :
7220 From: "Celia Langley" <7azynsvcrj@concentric.com> 7220 Subject: Fwd: Best Source V|@gra, Valï(u)m, X(a)n@x Diet Pills Any Meds bfdrsubre ti 7221 From: "PosMortgage" <quotes@arthriaway.com> 7221 Subject: One simple inquiry, multiple great quotes 7222 From: "Judson Culver" <ofzhywirrgldjj@lycos.com> 7222 Subject: plutonium classificatory buildup 7223 From: "Avis Rosado" <ferreroacquistapace3_342@china139.com> 7223 Subject: The penís pill is amazing you have to try it
On recharge la page à la main, pour constater que ça a marché ! Plus de messages en attente... Oups. Et s'il y avait des messages légitimes parmi ceux que nous venons d'effacer ?
Évidemment, tous les mails envoyés sur ma liste et bloqués pour cause de Post by non-member to a members-only list ne sont pas des spams. Il n'est donc pas envisageable de supprimer tous les messages d'un coup.
Pour s'assurer que la liste des messages ne contiendrait pas un message légitime, on peut remplacer la dernière ligne du script par les suivantes :
if (@ARGV) { $bot->click; warn "Removed $msg messages\n"; } else { warn "$msg messages on hold\n"; }
Ainsi, appeler le script sans paramètre se contenterait d'afficher le sujet et l'expéditeur du message, tandis qu'avec un paramètre (quelconque), tous les messages seraient supprimés. On peut alors vérifier rapidement qu'il n'y a pas de message légitime dans la liste avant de tout balancer aux ordures.
On pourrait égalament faire un affichage des messages bloqués les un après les autres, pour sélectionner l'action à engager, le choix par défaut étant Discard.
De plus, si Mailman est configuré pour demander l'approbation d'un modérateur pour chaque demande d'inscription, de nouveau champs vont apparaître dans notre formulaire (celui-ci s'appelle après tout Administrative requests et pas Blocked emails...).
Bref, il y a encore de quoi s'occuper avec Mailman et WWW::Mechanize !
Bien que je n'ai pas eu le temps (ou le courage) de faire un script complet pour gérer l'interface fastidieuse de Mailman (écrit en Python) avec Perl, nous avons tout de même pu voir les avantages à utiliser WWW::Mechanize :
pas une fois nous n'avons dû analyser du HTML et pourtant nous avons rempli de façon automatique des dizaines de champs
les messages sont effacés beaucoup plus rapidement que par un humain utilisant sa souris et l'interface web de mailman. En effet, le script a effacé lors de mes premiers tests 48 messages en 9 secondes ; à mon retour de 15 jours de vacances, il a supprimé 736 messages en 5 minutes et 30 secondes.
Le temps le plus long est pris par les délais de transfert : le formulaire validé est quasiment identique au formulaire reçu et celui-ci contient plusieurs kilo-octets de données par message.
Pour terminer cette série d'articles, voici plusieurs modules, scripts ou programmes qui peuvent vous aider lors de la création d'un script de navigation automatisée.
Nous avons déjà rencontré lwp-request dans le précédent article. Ce programme vous permettra de récupérer rapidement le contenu d'une page à traiter.
Il est également utile à mon avis quand il est utilisé avec les options -des, ce qui permet de se faire une idée rapide des en-têtes renvoyés par le serveur.
La méthode TRACE
peut également être utile pour détecter un problème
sur le chemin entre vous et le serveur.
wget, comme lwp-request va vous permettre de récupérer dans
un fichier le contenu d'une page web donnée. C'est un programme
GNU avec de très nombreuses options. Il peut chercher une simple
page ou peut faire la copie récursive de tout ou partie d'un site
si les liens ne sont pas cachés dans du javascript. Notons aussi
l'option -S
qui permet d'afficher les en-têtes de la réponse
du serveur.
Utilisation pour télécharger une seule page :
$ wget http://www.perlmonks.org/ --22:44:13-- http://www.perlmonks.org/ => `index.html' Résolution de www.perlmonks.org... 209.197.123.153, 66.39.54.27 Connexion vers www.perlmonks.org[209.197.123.153]:80...connecté. requête HTTP transmise, en attente de la réponse...200 OK Longueur: non spécifié [text/html] [ <=> ] 63,219 33.45K/s 22:44:19 (33.33 KB/s) - « index.html » sauvegardé [63219]
mech-dump est un script fourni avec WWW::Mechanize qui permet d'obtenir rapidement les informations utiles concernant une page web. Il fournit la liste des formulaires, liens et images d'une page en une seule commande :
$ mech-dump --forms http://www.google.fr/ GET http://www.google.fr/search q= ie=ISO-8859-1 (hidden) hl=fr (hidden) btnG=Recherche Google (submit) btnI=J'ai de la chance (submit) meta= (radio) [*|lr=lang_fr|cr=countryFR]
L'option --forms affiche la liste des formulaires, --links affiche la liste des liens (absolus avec l'option --absolute) et --images la liste des images. --all vous donnera évidemment tout ce qui précède.
HTTP::Proxy est un proxy web écrit en Perl pur. Il permet d'appliquer des filtres sur les données qui le traversent. Ces filtres permettent de lire et de modifier les en-têtes et le corps des requêtes et des réponses HTTP.
HTTP::Proxy peut donc vous être utile pour voir les requêtes envoyées par votre navigateur. Cependant, bien qu'il ait été écrit spécifiquement avec pour objectif de générer automatiquement des scripts de connexion au web à partir d'une session de surf, je ne pense pas qu'HTTP::Proxy puisse en l'état (version 0.12) servir seul à construire un tel script.
Si vous l'utilisez, ce sera à vous d'écrire les filtres pour extraire les informations qui vous intéressent. Voici un exemple simple pour obtenir les informations d'une requête POST :
#!/usr/bin/perl -w use strict; use HTTP::Proxy qw( :log ); use HTTP::Proxy::BodyFilter::simple; use CGI::Util qw( unescape ); # création du proxy et du filtre my $proxy = HTTP::Proxy->new; my $filter = HTTP::Proxy::BodyFilter::simple->new( sub { my ( $self, $dataref, $message, $protocol, $buffer ) = @_; print STDOUT $message->method, " ", $message->uri, "\n"; # ce code provient de CGI.pm, méthode parse_params() my (@pairs) = split ( /[&;]/, $$dataref ); for (@pairs) { my ( $param, $value ) = split ( '=', $_, 2 ); $param = unescape($param); $value = unescape($value); printf STDOUT " %-30s => %s\n", $param, $value; } } ); # le filtre ne s'applique qu'aux requêtes POST $proxy->push_filter( method => 'POST', request => $filter ); # démarrage du proxy $proxy->start;
Il ne reste plus qu'à indiquer à votre client d'utiliser comme proxy HTTP localhost, port 8080 (le port par défaut de HTTP::Proxy).
Vous verrez alors le contenu des requêtes POST :
POST http://www.pagesjaunes.fr/pb.cgi faire => decode_input_image DEFAULT_ACTION => bf_inscriptions_req SESSION_ID => GB-78B5F09-2B186 VID => GB-78B5F09-2B186 lang => FR pays => FR srv => PB TYPE_RECHERCHE => ZZZ input_image => FRM_NOM => martin FRM_PRENOM => jacques FRM_ADRESSE => FRM_LOCALITE => paris FRM_DEPARTEMENT => BF_INSCRIPTIONS_REQ.x => 60 BF_INSCRIPTIONS_REQ.y => 16
Voici un exemple plus complet, qui permet de voir l'ensemble
des requêtes GET et POST, les cookies envoyés par le server (Set-Cookie
)
et le client (Cookie
) et les paramètres des requêtes POST. Il nécessite
au moins la version 0.12 de HTTP::Proxy.
#!/usr/bin/perl -w use strict; use HTTP::Proxy qw( :log ); use HTTP::Proxy::HeaderFilter::simple; use HTTP::Proxy::BodyFilter::simple; use CGI::Util qw( unescape ); my $post_filter = HTTP::Proxy::BodyFilter::simple->new( sub { my ( $self, $dataref, $message, $protocol, $buffer ) = @_; print STDOUT $message->method, " ", $message->uri, "\n"; print_headers( $message, qw( Cookie Cookie2 )); # this is from CGI.pm, method parse_params my (@pairs) = split ( /[&;]/, $$dataref ); for (@pairs) { my ( $param, $value ) = split ( '=', $_, 2 ); $param = unescape($param); $value = unescape($value); printf STDOUT " %-30s => %s\n", $param, $value; } } ); my $get_filter = HTTP::Proxy::HeaderFilter::simple->new( sub { my ( $self, $headers, $message ) = @_; my $req = $message->request; if( $req->method ne 'POST' ) { print STDOUT $req->method, " ", $req->uri, "\n"; print_headers( $req, qw( Cookie Cookie2 )); } print_headers( $message, qw( Content-Type Set-Cookie Set-Cookie2 )); } ); sub print_headers { my $message = shift; for my $h (@_) { if( $message->header($h) ) { print STDOUT " $h: $_\n" for ( $message->header($h) ); } } } my $proxy = HTTP::Proxy->new; $proxy->logmask( shift || NONE ); $proxy->push_filter( method => 'POST', request => $post_filter ); $proxy->push_filter( response => $get_filter ); $proxy->start;
Ce code (disponible dans le fichier eg/logger.pl de la distribution HTTP::Proxy) donnera des résultats plus complets que le précedent pour une session :
GET http://www.pagesjaunes.fr/ Content-Type: text/html Set-Cookie: VisitorID=GB-78C7D2C-36ECC; expires=Monday, 05-Jan-2014 17:53:16 GMT; domain=.pagesjaunes.fr; path=/ GET http://ads.wanadooregie.com/html.ng/sn=pagesjaunes.vig&pn=accueil_popup.pj&sz=1x1&ord=9695738522? Cookie: NGUserID=a010828-24721-1073324900-6 Content-Type: text/html POST http://www.pagesjaunes.fr/pj.cgi Cookie: VisitorID=GB-78C7D2C-36ECC; e=P-mkj6wWAWgAAG6Jg8A faire => decode_input_image DEFAULT_ACTION => jf_inscriptions_req SESSION_ID => GB-78C7D2C-36ECC VID => GB-78C7D2C-36ECC lang => FR pays => FR srv => PJ TYPE_RECHERCHE => ZZZ input_image => FRM_ACTIVITE => plombier FRM_NOM => FRM_ADRESSE => FRM_LOCALITE => paris FRM_DEPARTEMENT => JF_INSCRIPTIONS_REQ.x => 0 JF_INSCRIPTIONS_REQ.y => 0 Content-Type: text/html
Il manque probablement quelques champs, mais ce n'est pas mal pour commencer. Le code des filtres reste cependant un peu compliqué à écrire.
HTTP::Recorder est un module très récent, écrit par Linda Julien. Il s'utilise avec HTTP::Proxy en remplaçant l'agent par défaut par un objet HTTP::Recorder, qui enregistre dans un fichier une suite de commandes pour WWW::Mechanize construite à partir des requêtes que l'agent a dû réaliser.
Le module en est pour le moment à l'état de « preuve de concept » (version 0.01 sur CPAN), mais donne déjà des résultats intéressants. Voici par exemple le fruit d'une recherche sur les pages blanches effectué avec le script donné en exemple dans la documentation :
$agent->get("http://www.pageblanches.fr/"); $agent->get("http://www.pagesjaunes.fr/pb.cgi?"); $agent->field("srv", "PB"); $agent->field("BF_INSCRIPTIONS_REQ.y", "6"); $agent->field("VID", "FA-70033A4-33F4C"); $agent->field("FRM_LOCALITE", "paris"); $agent->field("FRM_NOM", "martin"); $agent->field("DEFAULT_ACTION", "bf_inscriptions_req"); $agent->field("faire", "decode_input_image"); $agent->field("FRM_PRENOM", "jacques"); $agent->field("SESSION_ID", "FB-70033C8-3B40A"); $agent->field("lang", "FR"); $agent->field("TYPE_RECHERCHE", "ZZZ"); $agent->field("pays", "FR"); $agent->field("BF_INSCRIPTIONS_REQ.x", "50"); $agent->submit_form(form_number => "1");
Il ne reste plus qu'à supprimer les champs qui ont gardé leurs valeurs par défaut et surtout le plus difficile : extraire l'information utile de la page retournée par la dernière commande. Comme vous pourrez le constater, c'est souvent la partie la plus difficile, surtout quand les pages sont truffées de JavaScript.
Notez que si vous voulez réaliser automatiquement des recherches sur les services Pages Jaunes et Pages Blanches, ce n'est pas la peine de vous fatiguer à analyser le site web de France Télécom. En effet, il existe déjà un script Perl qui fait cela : le script pagesjaunes fourni avec le module WWW::Search::Pagesjaunes de Briac Pilpré (de Paris.pm) peut répondre à toutes vos questions plus rapidement que Mozilla :
$ pagesjaunes -n martin -p jacques -t paris Martin Jacques - 5 r Banquier 75013 PARIS - 01 43 36 07 12 Martin Jacques - 159 r Charenton 75012 PARIS - 01 43 44 25 59 Martin Jacques - 9 r Comète 75007 PARIS - 01 45 55 55 68 Martin Jacques - 30 bd Exelmans 75016 PARIS - 01 42 24 43 07 ...
Pour accéder aux Pages Jaunes, il suffit de préciser l'activité :
$ pagesjaunes -t paris -act plombier Bourdeaux Alain (EURL) - 11 r Emile Dubois 75014 Paris - .01 45 65 09 40 Bourdeaux Alain (EURL) - 11 r Emile Dubois 75014 Paris - .01 45 65 09 40 fax : . 01 45 81 53 52 Boutillon Stéphane - 26 r George Sand 75016 Paris - .01 42 24 90 40 Bruel et Terrage - 6 r Jeanne d'Arc 75013 PARIS - .01 45 83 99 01 ...
pagesjaunes est un excellent exemple de ce qu'on peut faire sur le web sans son navigateur, à l'aide de LWP::UserAgent ou de WWW::Mechanize.
Ce module, que j'ai découvert lors de la rédaction de cet article, semble très prometteur. Écrit par Max "Corion" Maischein, il fournit un genre de shell au dessus de WWW::Mechanize, pour vous permettre de surfer de façon interactive, en ligne de commande !
Reprenons encore une fois notre exemple avec perlmonks. Dans le shell de
WWW::Mechanize::Shell, l'invite de commande contient l'URL courante
suivie du caractère >
:
$ perl -MWWW::Mechanize::Shell -e shell Module File::Modified not found. Automatic reloading disabled. >get http://www.perlmonks.org/index.pl?node=login Retrieving http://www.perlmonks.org/index.pl?node=login(200) http://www.perlmonks.org/index.pl?node=login>eval $self->agent->form_number(2) HTML::Form=HASH(0x876bd40) http://www.perlmonks.org/index.pl?node=login>dump POST http://www.perlmonks.org/? op=login (hidden) node_id=109 (hidden) user= passwd= (password) expires=<UNDEF> (checkbox) [*<UNDEF>/off|+10y/save me a permanent cookie, cowboy!] sexisgood=submit (submit) .cgifields=expires (hidden) http://www.perlmonks.org/index.pl?node=login>fillout (text)user> [] BooK (password)passwd> [] s3kr3t |+10y (checkbox)expires> [] http://www.perlmonks.org/index.pl?node=login>submit 200 http://www.perlmonks.org/?>script
La commande script
permet d'obtenir le code WWW::Mechanize correspondant
aux commandes entrées tout au long de la session. Le script résultant
est le suivant :
#!/usr/bin/perl -w use strict; use WWW::Mechanize; use WWW::Mechanize::FormFiller; use URI::URL; my $agent = WWW::Mechanize->new( autocheck => 1 ); my $formfiller = WWW::Mechanize::FormFiller->new(); $agent->env_proxy(); $agent->get('http://www.perlmonks.org/index.pl?node=login'); $agent->form(1) if $agent->forms and scalar @{$agent->forms}; print( do { $agent->form_number(2) },"\n" ); $formfiller->add_filler( 'user' => Fixed => 'BooK' ); $formfiller->add_filler( 'passwd' => Fixed => 's3kr3t' ); $formfiller->add_filler( 'expires' => Fixed => '' );$formfiller->fill_form($agent->current_form); $agent->submit();
Je vous invite à consulter la documentation de ce module pour plus de détails, car je ne fais moi-même que découvrir ses possibilités.
Un peu plus bas dans les couches réseau, ngrep (pour network grep) permet de voir le contenu des paquets qui circulent sur une interface réseau. Il est tout de même assez rare d'en avoir besoin pour un script web.
Exemple :
# ngrep -d ppp0 -n 2 port 80 interface: ppp0 (213.102.24.231/255.255.255.255) filter: ip and ( port 80 ) ##### T 213.102.24.231:3341 -> 193.252.242.142:80 [AP] GET / HTTP/1.1..TE: deflate,gzip;q=0.3..Keep-Alive: 300..Connection: Keep-A live, TE..Cache-Control: max-age=0..Via: 1.1 rose (HTTP::Proxy/0.12)..Accep t: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plai n;q=0.8,video/x-mng,image/png,image/jpeg,image/gif;q=0.2,text/css,*/*;q=0.1 ..Accept-Charset: ISO-8859-1, utf-8;q=0.66, *;q=0.66..Accept-Language: fr, en;q=0.50..Host: www.pagesjaunes.fr..User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.0.0) Gecko/20020623 Debian/1.0.0-0.woody.1.... # T 193.252.242.142:80 -> 213.102.24.231:3341 [AP] HTTP/1.1 200 OK..Server: Netscape-Enterprise/4.1..Date: Sun, 04 Jan 2004 21 :42:44 GMT..Content-type: text/html..P3p: Policyref="http://www.pagesjaunes .fr/w3c/p3p.xml", CP="NON DSP COR LAW CUR ADMa DEVa HISa OUR LEG UNI COM NA V INT CNT STA"..Set-cookie: VisitorID=GD-78B6174-436CE; expires=Sunday, 04- Jan-2014 21:42:44 GMT; domain=.pagesjaunes.fr; path=/..Content-length: 2163 1.... ##exit 8 received, 0 dropped
C'est généralement beaucoup plus d'information que vous n'en avez réellement besoin pour la rédaction de votre script, mais cela peut être utile pour savoir ce qui se passe réellement.
Au cours de ces trois articles, je vous ai fait découvrir LWP::UserAgent, le principal module utilisé pour se connecter au web en Perl, puis les techniques courantes d'authentification et de récupération d'informations, avant de vous présenter un ensemble de scripts et de modules qui simplifient encore la rédaction de robots du web. Sans compter les multiples exemples concrets de code.
Il y a encore un certain nombre de modules et de techniques qui restent à présenter, mais avec ce que vous avez appris et le secours de la documentation, il y a peu de sites web qui devraient vous resister longtemps.
http://search.cpan.org/dist/WWW-Mechanize/ est la page où vous trouverez la documentation et le code de WWW::Mechanize.
La page du projet sur SourceForge ne contient en fait qu'un accès en lecture au dépôt CVS du projet.
La page de manuel WWW::Mechanize::Examples(3) contient des exemples de scripts fournis par des utilisateurs de WWW::Mechanize.
Les deux articles précédents ont été publiés dans Linux Magazine 56 et 57.
Tous les articles publiés par les mongueurs de Perl sont disponibles en ligne une fois que Linux Magazine n'est plus en kiosque. Consultez http://articles.mongueurs.net/magazines/ pour voir la liste des articles disponibles.
Les activités du groupe de travail « articles » des Mongueurs de Perl sont décrites à la page http://articles.mongueurs.net/.
Un certain nombre de modules utilisent WWW::Mechanize pour s'interfacer avec des sites web comme WWW::Google::Groups et WWW::Yahoo::Groups, qui permettent d'accéder aux messages de ces groupes, ou Finance::Bank::CreditMut et Finance::Bank::BNPParibas (créés par deux mongueurs) qui vous permettent d'accéder aux informations de votre compte bancaire en ligne.
D'autres modules dérivent de WWW::Mechanize pour proposer des versions spécialisées de ce module : WWW::Mechanize::Cached (utilise Cache::Cache comme cache afin de ne pas surcharger les serveurs web), WWW::Mechanize::Sleepy (un robot qui « dort » entre deux requêtes, afin de ne pas trop surcharger les serveurs web) et WWW::Mechanize::SpamCop (qui se charge de signaler des spams au site http://www.spamcop.net/).
Enfin, certains modules sont destinés à compléter la boîte à outils WWW::Mechanize, en proposant des fonctionnalités supplémentaires ou en permettant de construire des scripts WWW::Mechanize à partir de sessions web : WWW::Mechanize::Shell (qui donne un shell à utiliser au dessus de WWW::Mechanize) et WWW::Mechanize::FormFiller (remplissage automatique de formulaires, utilisé par WWW::Mechanize::Shell) et HTTP::Recorder.
Philippe 'BooK' Bruhat, <book@mongueurs.net>.
Philippe Bruhat est vice-président de l'association les Mongueurs de Perl et membre du groupe Paris.pm. Il est consultant spécialisé en sécurité et l'auteur des modules Log::Procmail, HTTP::Proxy et Regexp::Log, disponibles sur CPAN.
Merci aux membres du groupe de travail « articles » des Mongueurs de Perl pour leur relecture attentive malgré la date tardive de remise de cet article.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.