Article publié dans Linux Magazine 56, décembre 2003.
Copyright © 2003 - Philippe Bruhat.
libwww-perl
L'acquisition de données et surtout la gestion d'applications à travers le web sont généralement une occupation fastidieuse et répétitive. De nombreux outils disposent uniquement d'une interface web et on se rend souvent compte que l'on irait plus vite avec un petit script en ligne de commande. Cette série d'articles va vous apprendre comment automatiser la navigation HTTP en Perl.
Le problème principal du web, c'est que les navigateurs ne sont pas très pratiques à utiliser. D'une certaine façon, il faut bien reconnaître qu'au niveau interface utilisateur, ce n'est souvent pas terrible et que les formulaires HTML montrent rapidement leurs limites en matière d'ergonomie (« Oh, encore un formulaire où il faut cliquer 397 fois avant de pouvoir valider ! »).
Dans de nombreux cas on préférerait pouvoir se passer de son navigateur, en particulier pour toutes les situations répétitives : récupération d'une information régulièrement mise à jour, vérification qu'une page web a été modifiée ou non, ou tout simplement obtention d'une information texte simple à partir d'un site à la présentation alambiquée.
Après tout, puisque HTTP n'est qu'un protocole réseau, il doit donc être possible d'utiliser directement le protocole pour parler avec le serveur et obtenir de lui ce que l'on souhaite. Voici une première tentative :
#!/usr/bin/perl -w use strict; use Socket; my ( $iaddr, $paddr, $proto ); # ouvre la connexion $iaddr = inet_aton('paris.mongueurs.net') or die "Le serveur n'existe pas"; $paddr = sockaddr_in( 80, $iaddr ); $proto = getprotobyname('tcp'); socket( SOCK, PF_INET, SOCK_STREAM, $proto ) or die "socket: $!"; connect( SOCK, $paddr ) or die "connect: $!"; # force le vidage automatique du tampon select( ( select(SOCK), $| = 1 )[0] ); # envoie la requête # ("\r\n" n'est pas portable, d'où l'utilisation de "\015\012") print SOCK "GET / HTTP/1.0\015\012Host: paris.mongueurs.net\015\012\015\012"; # récupère la réponse my $page = do { local $/; <SOCK> }; # ferme la connexion close(SOCK) or die "close: $!";
Rappel : Mettre $/
à undef
permet de passer en mode slurp
(aspiration), et de capturer ainsi toute la page d'un seul coup. Il ne
reste alors plus qu'à traiter le contenu de $page
.
Note : Dans tous les scripts de cet article, j'utilise l'option
-w et la pragma strict
, afin de détecter rapidement les fautes
d'inattention, les typos, ainsi que les problèmes liés à ma lecture trop
rapide des documentations. En général, cela permet de détecter plus
rapidement les erreurs et de les corriger derechef. Tous ces scripts
ont été testés et utilisés par moi avant d'être inclus dans l'article.
Il est tout de même un peu fastidieux d'ouvrir ses connexions à la main. Heureusement, Perl dispose de classes réseau d'un peu plus haut niveau, comme IO::Socket::INET, qui encapsulent les fonctions complexes et permettent d'ouvrir facilement des connexions vers tout serveur sur Internet.
#!/usr/bin/perl -w use strict; use IO::Socket::INET; # ouvre une connexion sur le serveur my $sock = IO::Socket::INET->new( PeerAddr => 'paris.mongueurs.net', PeerPort => 'http(80)', Proto => 'tcp' ) or die "Impossible de se connecter"; # envoie la requête print $sock "GET / HTTP/1.0\015\012Host: paris.mongueurs.net\015\012\015\012"; # récupère la réponse my $page = do { local $/; <$sock> }; # ferme la connexion close $sock or die "close: $!";
Dans les deux exemples précédents, $page
contient toute la
réponse : la ligne de statut (200 OK
), les en-têtes et le corps
du message. Il vous reste encore tout le travail d'analyse (certes pas
très compliqué) à faire pour séparer ces trois éléments. Et le véritable
traitement n'a pas encore commencé !
Sans compter le fait que, si vous passez par un serveur mandataire (ou proxy), il vous faudra modifier la connexion (on se connecte au proxy, et non plus directement au serveur final) et la requête (l'URI doit être absolue et non relative).
Bref, si vous utilisez Socket ou IO::Socket::INET pour gérer vos connexions manuellement, votre petit script sera obèse avant d'être utile.
Enfin, nous avons utilisé ici le protocole HTTP/1.0, car le support de HTTP/1.1 (connexions persistantes, chunked encoding, etc.) est beaucoup plus compliqué qu'une simple ouverture de connexion. En réalité, HTTP est un protocole complexe, pour lequel on compte pas moins de 25 RFC.
Il est donc préférable d'utiliser une librairie qui respecte au mieux
les protocoles, plutôt que d'en faire sa propre implémentation bancale.
Perl dispose d'une telle librairie : libwwww-perl
, plus connue sous
l'abréviation LWP.
libwww-perl
Pour accéder au web en Perl, il est heureusement inutile d'ouvrir vos
connexions vous-mêmes et d'envoyer des GET / HTTP/1.0
dessus. Ce
serait ré-inventer une roue particulièrement ronde : la librairie
libwww-perl
ou LWP
. Il s'agit d'un ensemble de modules Perl pour
accéder au web (et plus), dont la création et le développement ont été
coordonnés par Gisle Aas depuis 1995, à partir de la librairie
libwww-perl écrite pour Perl 4 par Roy Fielding (l'un des auteurs du RFC
concernant HTTP). Comme vous allez le voir dans la suite de cet article,
LWP est l'outil indispensable pour l'écriture de clients HTTP en Perl.
LWP est une librairie orientée objet qui utilise beaucoup les notions d'héritage et d'encapsulation et s'appuie sur le modèle de communication HTTP. C'est-à-dire que la communication se déroule toujours de la même façon : une requête (un objet HTTP::Request) est construite, puis envoyée au serveur qui renvoie une réponse (un objet HTTP::Response), qui contient les informations requises. HTTP est un protocole sans mémoire, ce qui signifie que chaque requête est traitée indépendamment des autres. LWP s'appuie sur ce modèle de communication pour réaliser non seulement des requêtes HTTP, mais aussi ftp, gopher, SMTP, des accès au système de fichier local, etc.
Cette partie va introduire quelques-uns des modules principaux de la librairie LWP. Ne vous inquiétez pas si tout ne vous paraît pas clair au début, cette série d'articles se poursuivra par des exemples largement commentés. En ce qui concerne l'API complète (liste des méthodes et attributs), je vous renvoie à la documentation en ligne.
C'est le module principal. Il s'agit d'un client qui supporte de nombreux protocoles, à travers le modèle HTTP (une requête donnant lieu à une réponse). Afin de pouvoir travailler avec le protocole HTTP, de nombreux modules objets (pour la plupart décrits ci-après) représentent les divers éléments d'une connexion.
C'est un objet LWP::UserAgent qui prend votre requête
(HTTP::Request) et la passe au serveur pour obtenir une réponse
(HTTP::Response). L'initialisation se fait comme d'habitude, avec
la méthode new()
.
my $ua = LWP::UserAgent->new( agent => "MonAgent/0.02" );
Notez que le constructeur de LWP::UserAgent accepte des options comme
agent
ou timeout
, qui peuvent également être mises à jour
avec les accesseurs correspondants (agent()
, timeout()
, etc.).
LWP::UserAgent dispose de plusieurs méthodes pour exécuter les
requêtes : send_request()
, simple_request()
et request()
.
Les différences entre ces méthodes sont liées à l'existence
ou non d'un pré-traitement de la requête et du suivi automatique
des redirections (codes HTTP 3xx).
Nous ne rentrerons pas dans les détails des protocoles autres que HTTP, mais sachez que LWP supporte également les protocoles ftp, gopher et autres de la même façon, à l'aide d'objets HTTP::Request et HTTP::Response. Les menus ftp et gopher sont même convertis en HTML au vol.
Ces deux modules sont des sous-classes de HTTP::Message destinées à encapsuler les requêtes HTTP et les réponses associées. HTTP::Message est leur classe ancêtre commune et abstraite qui gère la logique en-tête et corps de message.
HTTP::Request dispose d'accesseurs comme method()
, uri()
,
content()
(le corps de la requête, utilisé par exemple avec la méthode
POST) ou headers()
(qui donne accès à l'objet HTTP::Headers représentant
les en-têtes du message).
HTTP::Response fournit (entre autres) les accesseurs code()
(code de
statut de la réponse), content()
(le corps de la réponse, en général
la page web demandée) ou headers()
(encore les en-têtes). Le module
dispose également de fonctions utilitaires comme is_success()
,
is_error()
ou is_redirect()
qui permettent de connaître l'état de
la réponse sans forcément regarder son code (200
, 404
, etc.).
Ce module encapsule les en-têtes d'un message HTTP, et vous donne accès à ceux-ci.
Il dispose de méthodes d'accès aux différents en-têtes :
# $req est un objet HTTP::Request my $h = $req->headers(); # affiche "libwww-perl/5.69" print $h->header( 'User-Agent' ); # modifie un en-tête $h->header( User_Agent => 'Mozilla/5.0' ); # supprime un en-tête $h->remove_header( 'Referer' );
Notez que pour les en-têtes contenant un tiret (comme User-Agent
, ou
Content-Type
), on peut remplacer celui-ci par un caractère souligné
(underscore ou _
), afin de se passer de guillemets lors de
l'affectation d'un en-tête. En effet, l'opérateur =>
fournit un
contexte de chaîne à sa gauche à condition de suivre un mot simple
(bareword).
Parmi les en-têtes HTTP se trouvent ceux qui gèrent les cookies. Ce module permet à votre client de gérer automatiquement les cookies reçus et envoyés, de les sauver et de les restaurer au prochain lancement de votre script, ou à partir du fichier de cookies de votre navigateur.
L'utilisation de ce module est expliquée en détail dans cet article.
HTML::Form donne aux formulaires HTML une interface objet. Les objets HTML::Form sont créés directement à partir d'un document HTML :
@forms = HTML::Form->parse( $document_html, $base_uri )
Le tableau @forms
contient un objet HTML::Form par formulaire
contenu dans le document HTML. Afin que l'on puisse ensuite construire
la requête correspondant à la validation du formulaire, il est
indispensable de fournir l'URI ayant permis d'obtenir le document.
Il existe un constructeur new()
, mais il est en général peu utilisé.
Un formulaire contient un certain nombre de champs d'entrée, qui sont des objets de type HTML::Form::Input (éventuellement plus spécialisés). Diverses méthodes permettent d'accéder aux champs et de les modifier.
# retourne la liste des champs du formulaire my @inputs = $form->inputs(); # trouve le champ nommé 'password' my $input = $form->find_input('password');
Une fois qu'on dispose d'une référence à un champ du formulaire, on peut l'utiliser comme dans son navigateur habituel :
# change le mot de passe (les deux formes sont équivalentes) $form->find_input('password')->value('S3kr3t'); $form->value( password => 'S3kr3t' );
Parmi les méthodes disponibles, on trouve : type()
, qui renvoie
le type de champ (c'est-à-dire button
, checkbox
, file
, hidden
,
image
, option
, password
, radio
, reset
, submit
ou
textarea
), name()
et value()
(ce sont des accesseurs qui
permettent d'obtenir ou de modifier le nom ou la valeur courante du champ),
possible_values()
, qui renvoie la liste des valeurs possibles (ou une
liste vide, si le champ ne propose pas un liste prédéfinie de valeurs).
Enfin, la méthode click()
permet de valider le formulaire, et
renvoie un objet HTTP::Request, directement utilisable par votre
LWP::UserAgent.
La librairie LWP contient même une classe qui permet de créer des serveurs HTTP de façon simple et rapide. Ce n'est pas Apache, bien sûr, mais c'est parfois suffisant.
LWP::Simple est un module à l'interface fonctionnelle qui permet de répondre aux besoins simples de connexion (récupérer le contenu d'une page web). Très utile pour des opérations simples, il montre rapidement ses limites.
get()
retourne le document correspondant à l'URL passé en paramètre,
et undef
en cas d'erreur.
# $url est une chaîne ou un objet URI my $page = get($url);
head()
permet de récupérer certaines informations à partir d'une
requête HEAD sur l'URL demandée. En contexte de liste, on obtient (dans
l'ordre) le type du message (Content-Type
), la taille du document
(Content-Length
), la date de dernière modification (Last-Modified
),
la date d'expiration (Expires
) et l'identification du serveur
(Server
) en cas de succès et une liste vide en cas d'échec.
$ perl -MLWP::Simple -le '$,=$/;print head"http://www.mongueurs.net/"' text/html; charset=iso-8859-1 3797 1066569963 Apache/1.3.27 (Unix)
En contexte scalaire, la fonction retourne une valeur vraie ou fausse selon que le document est disponible ou non.
getprint()
fonctionne comme get()
mais imprime le contenu de
la page demandée sur STDOUT. En cas d'erreur, imprime le code d'erreur
et le message associé. La valeur de retour est le code de réponse HTTP.
getstore()
enregistre le corps de la réponse dans le fichier passé
en paramètre, et retourne le code de réponse HTTP.
mirror()
fait la même chose que getstore()
, mais s'appuie sur
l'en-tête de requête If-modified-Since
et sur l'en-tête de réponse
Content-Length
.
Dès que vous avez besoin d'un peu plus de détails sur la réponse ou dans l'élaboration de la requête (pour faire un POST, par exemple), c'est le signe qu'il est temps d'utiliser LWP::UserAgent.
Le chargement de ce module force LWP::UserAgent à afficher ce qu'il fait. Il est très utile pour le débogage d'un script.
On peut afficher (ou non) trois niveaux de messages : trace
(appels de fonctions), debug
(messages de débogage) et conns
(les
données transférées par les connexions). La fonction level()
permet de
contrôler ce qu'on veut afficher, en lui passant en paramètre le nom du
niveau de log désiré, préfixé d'un +
ou d'un -
. Il est également
possible de passer directement les paramètres lors de l'importation
du module.
# affiche tout use LWP::Debug qw(+); # n'affiche rien use LWP::Debug qw(-); # affiche tout sauf les connexions use LWP::Debug qw(+ -conns); # utilisation de la fonction level() use LWP::Debug qw(level); level('+'); level('-conns');
Voici un exemple de ce que LWP::Debug peut afficher pour un simple
appel à la méthode getprint()
de LWP::Simple :
$ perl -MLWP::Debug=+ -MLWP::Simple -e 'getprint"http://www.mongueurs.net/"' LWP::UserAgent::new: () LWP::UserAgent::request: () LWP::UserAgent::send_request: GET http://www.mongueurs.net/ LWP::UserAgent::_need_proxy: Not proxied LWP::Protocol::http::request: () LWP::Protocol::collect: read 755 bytes <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> ...
Voici donc comment récupérer la page principale du site de Paris.pm avec LWP::Simple :
#!/usr/bin/perl -w use strict; use LWP::Simple; # récupère le corps de la réponse my $content = get('http://paris.mongueurs.net/');
Difficile de faire plus simple ! Sa simplicité est en fait la
principale limitation de LWP::Simple : il ne donne pas accès aux
en-têtes, et la méthode get()
retourne un simple undef
en cas d'erreur.
Il est donc impossible de savoir si on a affaire à une erreur 404 ou à un
dépassement du délai de garde de la connexion (timeout).
Pour accéder à toutes les informations concernant votre connexion HTTP, il vous faudra utiliser LWP::UserAgent et les objets qu'il emploie :
#!/usr/bin/perl -w use strict; use LWP::UserAgent; # crée un agent et une requête my $ua = LWP::UserAgent->new(); my $req = HTTP::Request->new( GET => 'http://paris.mongueurs.net/' ); # récupère la réponse my $res = $ua->request($req);
L'objet $res
retourné par la méthode request()
est de type
HTTP::Response. Vous pouvez obtenir l'objet HTTP::Headers qui représente
les en-têtes de cette réponse avec la méthode headers()
et le corps
du message avec la méthode content()
.
Nous allons utiliser LWP::UserAgent et les librairies de LWP pour nous connecter au site Perlmonks (accessible à http://www.perlmonks.org/). En effet, le site récompense les utilisateurs fidèles en leur donnant des point d'expérience (XP) de façon aléatoire à la première connexion de la journée. Si un utilisateur se connecte tous les jours, son expérience augmente un peu plus vite.
Les points d'expérience sont également attribués en de nombreuses autres occasions (votes sur les messages postés par soi-même ou les autres utilisateurs). Utiliser un script de connexion automatique permet de s'assurer que l'on touchera quelques XP même quand on n'a pas le temps matériel de se connecter sur le site.
Notre objectif est donc de nous connecter au moins une fois par jour, avec cron et perl. Voici le script qui nous permet de le faire :
#!/usr/bin/perl -w use strict; use LWP::UserAgent; use HTML::Form; # les paramètres personnels my ( $user, $pass ) = qw( BooK S3kr3t ); my $base = 'http://www.perlmonks.org/'; # initialisation de l'agent my $ua = LWP::UserAgent->new( agent => 'Mozilla/4.73 [en] (X11; I; Linux 2.2.16 i686; Nav)' ); # création de la requête my $req = HTTP::Request->new( GET => "${base}index.pl?node=login" ); # exécute la requête et reçoit la réponse my $res = $ua->request($req); die $res->status_line if not $res->is_success; # le formulaire de login est le second formulaire de la page my $form = ( HTML::Form->parse( $res->content, $base ) )[1]; # remplit les champs $form->find_input('user')->value($user); $form->find_input('passwd')->value($pass); # valide et renvoie le formulaire $ua->request( $form->click );
L'initialisation des variables jusqu'à l'appel de $ua->request()
ne pose pas de problème. On essaie de passer inaperçu en indiquant un
navigateur « standard ». ;-)
La méthode is_success()
de HTTP::Response
nous permet de vérifier que le serveur nous a bien répondu par un code
2xx (en général 200 OK
).
Si la réponse est correcte, nous allons alors remplir le formulaire
de connexion qu'elle contient. La classe HTML::Form permet de traiter
un formulaire HTML comme un objet. HTML::Form->parse()
retourne
une liste d'objets HTML::Form à partir d'un formulaire en HTML et
de l'URI de base ($base
dans notre exemple). Nous extrayons donc
le second formulaire (indice 1 dans la liste) et le stockons dans
$form
. Le premier formulaire est celui du moteur de recherche du site
perlmonks.
La méthode find_input()
de HTML::Form retourne un objet
HTML::Form::Input à partir de son nom. La méthode value()
de
HTML::Form::Input permet de modifier la valeur du champ correspondant
du formulaire. C'est ainsi que nous pouvons entrer nos login et
mot de passe dans le formulaire.
La méthode click()
retourne directement l'objet
HTTP::Request qui pourra être utilisé par LWP::UserAgent pour aller
chercher la page correspondant à la validation du formulaire.
Comme la réponse ne nous intéresse pas (nous voulons juste que perlmonks note notre connexion du jour, et nous attribue éventuellement un XP), le programme est fini.
Notez bien que nous faisons plusieurs tests d'erreur, afin de nous assurer que tout fonctionne. Sans un minimum de traitement d'erreur, tout script peut planter horriblement dès qu'un petit quelque chose ne se déroule pas comme prévu.
Le site perlmonks utilise des cookies pour conserver l'authentification des utilisateurs pendant leur navigation d'une page à l'autre. Le script précédent charge deux pages (récupération du formulaire, puis validation de celui-ci), alors que si nous connaissions le cookie utilisé, une seule requête suffirait : nous montrerions le biscuit en chargeant n'importe quelle page du site (si possible une page pas trop grosse).
HTTP::Cookies est capable de lire et de renvoyer les cookies reçus
depuis un serveur, et de les stocker dans un format qui lui est propre.
Pour cela, il suffit d'ajouter un attribut cookie_jar
à votre
objet LWP::UserAgent :
$ua->cookie_jar( HTTP::Cookies->new() );
L'objet client sera ainsi capable de traiter les cookies reçus, de les renvoyer avec les prochaines requêtes sur les sites correspondants, de les sauver dans un fichier, etc.
Le problème principal avec les cookies, c'est que la méthode
set_cookie()
de HTTP::Cookie n'a pas une signature très simple :
$cookie_jar->set_cookie( $version, $key, $val, $path, $domain, $port, $path_spec, $secure, $maxage, $discard, \%rest );
Plutôt que de capturer le cookie et de mettre à jour l'objet manuellement, il est préférable (la paresse est une vertu, souvenez-vous) de laisser LWP faire le travail à votre place.
Reprenons l'initialisation de notre objet LWP::UserAgent :
my $ua = LWP::UserAgent->new( agent => 'Mozilla/4.73 [en] (X11; I; Linux 2.2.16 i686; Nav)', cookie_jar => HTTP::Cookies->new( file => 'cookies.txt', autosave => 1 ) );
Nous ajoutons ainsi à notre agent une « boîte à cookies » où il stockera ceux qu'il reçoit. À la destruction de l'objet HTTP::Cookies, la liste des cookies sera sauvée dans le fichier cookies.txt.
Relançons notre script, et voyons le contenu du fichier cookies.txt ainsi généré :
#LWP-Cookies-1.0
Il n'y a aucun cookie. Pourtant, mon navigateur habituel me demande si j'accepte le cookie de perlmonks à chaque fois que je me connecte ! Un bref coup d'œil à la documentation de HTTP::Cookies nous donne la réponse :
$cookie_jar = HTTP::Cookies->new; The constructor takes hash style parameters. The following parameters are recognized: file: name of the file to restore cookies from and save cookies to autosave: save during destruction (bool) ignore_discard: save even cookies that are requested to be discarded (bool) hide_cookie2: do not add Cookie2 header to requests
Le paramètre ignore_discard
semble correspondre à notre problème.
En effet, le cookie renvoyé par perlmonks est certainement un cookie de session
qui n'est pas sauvegardé automatiquement par HTTP::Cookie
. Vous
remarquerez au passage que la librairie LWP respecte le RFC 2965 en ne
conservant pas ce cookie après la fin de l'exécution du script
(le comportement par défaut en l'absence du paramètre Max-Age
dans le cookie est de le détruire quand le client s'arrête).
Nous modifions donc notre code comme suit :
my $ua = LWP::UserAgent->new( agent => 'Mozilla/4.73 [en] (X11; I; Linux 2.2.16 i686; Nav)', cookie_jar => HTTP::Cookies->new( file => 'cookies.txt', autosave => 1, ignore_discard => 1 ) );
Après une nouvelle exécution de notre script, le fichier contient bien le cookie au format LWP-Cookies :
#LWP-Cookies-1.0 Set-Cookie3: userpass=BooK%257CBoO5RNugWibsc%257C; path="/"; domain=www.perlmonks.org; path_spec; discard; version=0
(Inutile d'essayer ce cookie pour me voler mon compte sur perlmonks :
j'ai changé mon mot de passe le temps des tests. ;-)
)
Nous allons donc pouvoir nous servir de ce cookie pour économiser un peu les ressources réseau. Nous utilisons également Getopt::Std pour gérer des options de ligne de commande :
#!/usr/bin/perl -w use strict; use LWP::UserAgent; use HTML::Form; 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 "Mauvais arguments"; # initialisation de l'agent my $ua = LWP::UserAgent->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 ) ); my $req = HTTP::Request->new( GET => "${base}index.pl?node=login" ); # nouvel utilisateur ou pas ? # l'option -l permet de forcer cette séquence, # qui capture et stocke le cookie if ( $conf{l} ) { my ( $user, $pass ) = split ':', $conf{l}, 2; # exécute la requête et reçoit la réponse my $res = $ua->request($req); die $res->status_line if not $res->is_success; # le formulaire de login est le second formulaire de la page my $form = ( HTML::Form->parse( $res->content, $base ) )[1]; # on remplit les champs $form->find_input('user')->value($user); $form->find_input('passwd')->value($pass); $ua->request( $form->click ); } # sinon on établit la connexion (avec le cookie s'il existe) else { $ua->request($req); }
À propos de ce script, il faut savoir qu'il n'affichera pas d'erreur si le mot de passe est mauvais ou si le cookie n'existe pas. Mais en quelques lignes de code, il rend déjà bien service.
Encore une dernière remarque au sujet des cookies : votre navigateur enregistre lui aussi des cookies sur le disque. Pourquoi ne pas aller chercher celui qui vous intéresse directement dans le fichier correspondant ?
Vous trouverez, dans la distribution standard de LWP ou sur CPAN, des modules qui se chargent de lire les cookies stockés au format propriétaire de votre navigateur et de les ajouter à un objet dérivé de HTTP::Cookies, celui-ci pouvant être utilisé par votre agent. Ceci pourra par exemple vous permettre de surfer avec votre navigateur habituel, puis de convertir les cookies qu'il aura récupérés au format HTTP::Cookies, afin de les utiliser dans vos scripts. Et vous n'aurez pas besoin de faire vous même la conversion entre le format natif de votre navigateur et le format interne HTTP::Cookies avant utilisation.
Les modules existants à ce jour sont : HTTP::Cookies::Netscape, HTTP::Cookies::Mozilla, HTTP::Cookies::Microsoft, HTTP::Cookies::iCab, HTTP::Cookies::Safari et HTTP::Cookies::Omniweb.
Voici un exemple de script de conversion des cookies Mozilla en cookies LWP. Il traduit le contenu du fichier Mozilla-cookies.txt (format Mozilla) et le sauve dans un fichier cookies.txt utilisable directement par le module HTTP::Cookies :
#!/usr/bin/perl -w use strict; use HTTP::Cookies::Mozilla; my $cookies = HTTP::Cookies::Mozilla->new( file => 'Mozilla-cookies.txt' ); HTTP::Cookies::save( $cookies, 'LWP-cookies.txt' );
La dernière ligne force l'objet $cookies
à être sauvegardé
avec la méthode save()
de HTTP::Cookies, et non celle de
HTTP::Cookies::Mozilla (qui sauve les cookies au format Mozilla).
Pour adapter ce script à votre navigateur,
il suffit simplement d'utiliser le module correspondant.
LWP::UserAgent supporte l'utilisation d'un serveur mandataire (proxy).
C'est la méthode proxy()
qui permet de le définir :
$ua->proxy( 'http', 'http://proxy.example.com:3128/');
Si le proxy supporte plusieurs protocoles, il faut utiliser une référence à un tableau :
$ua->proxy(['http', 'ftp'], 'http://proxy.example.com:8080/');
Si un mot de passe est nécessaire pour utiliser le proxy, la chaîne de
description correspondante sera :
http://user:pass@proxy.example.com:8080/
.
Il est également possible d'utiliser une variable d'environnement pour définir le proxy à utiliser.
Pour utiliser les variables d'environnement, il faut initialiser l'agent de la façon suivante :
my $ua = LWP::UserAgent->new( env_proxy => 1 );
La variable d'environnement utilisée par LWP::UserAgent est HTTP_PROXY
(FTP_PROXY
pour le proxy ftp, etc.). Notez que LWP::UserAgent ne tient
pas compte de la casse : il retrouvera son proxy que la variable
d'environnement s'appelle http_proxy
, HTTP_PROXY
ou encore
HtTp_PrOxY
.
L'avantage d'utiliser une variable d'environnement est qu'il est alors
possible d'écrire un script très général, qu'un utilisateur devant
utiliser un proxy pourra utiliser sans le modifier. Mais on peut tout
aussi bien ajouter une option --proxy
à son script (au sujet
des options, voir l'article de Jérôme Quelin dans Linux Magazine 49).
HTML::Form est assez simple à utiliser avec un formulaire de recherche classique, où il s'agit en général de remplir le champ texte correspondant à la requête.
Pour une simple requête Google, par exemple,
#!/usr/bin/perl -w use strict; use LWP::UserAgent; use HTML::Form; my $ua = LWP::UserAgent->new( env_proxy => 1, agent => "Mozilla/5.0" ); my $base = 'http://www.google.fr/'; my $res = $ua->request( HTTP::Request->new( GET => $base ) ); my $form = HTML::Form->parse( $res->content ); $form->value( q => shift ); $res = $ua->request( $form->click );
Et $res
contient la réponse HTTP faite par Google à votre requête.
Note : Google s'appuie sur l'en-tête User-Agent
pour refuser les
requêtes automatiques ;
c'est pourquoi nous avons utilisé l'attribut
agent
. De toute façon, si vous voulez faire des requêtes
sur Google de façon automatique, les conditions d'utilisation de Google
(http://www.google.com/terms_of_service.html) l'interdisent. Pour cela,
il est recommandé d'utiliser les API que Google met à la disposition
des développeurs.
Dans la suite de cette partie, nous allons voir comment valider les
champs ayant un nombre limité de valeurs possibles. Ils sont de type
radio
, checkbox
ou select
; les objets correspondants sont
tous de la classe HTML::Form::ListInput. Ces champs portent tous le même
nom, ce qui peut poser problème, puisque la fonction value()
(utilisée dans l'exemple $form->value( q => "test" )
cité
précédemment) ne permet que de modifier le premier champ de même
nom.
Nous allons donc voir comment remplir un formulaire à partir d'un petit exemple de source HTML :
<form method="POST" action="/sondage.pl"> <p>Quelles sont vos couleurs préférées ?</p> <p> <input type="checkbox" name="RGB" value="rouge" /> Rouge <input type="checkbox" name="RGB" value="vert" /> Vert <input type="checkbox" name="RGB" value="bleu" /> Bleu </p><p> <input type="radio" name="CYMK" value="cyan" checked /> Cyan <input type="radio" name="CYMK" value="jaune" /> Jaune <input type="radio" name="CYMK" value="magenta" /> Magenta </p> <p>Et vos fruits et légumes préférés ?</p> <select name="FRUITS" multiple> <option value="pommes">pommes <option value="poires">poires <option value="bananes">bananes <option value="oranges">oranges </select> <select name="LEGUMES"> <option value="brocolis">brocolis <option value="carottes">carottes <option value="courgettes">courgettes </select> <p><input type="submit" value="Valider" /></p> </form>
Voici ce qu'un navigateur affichera en le recevant :
Une fois l'objet $form
créé avec HTML::Form->parse()
, on peut
l'afficher avec sa méthode dump()
:
POST http://example.com/sondage.pl RGB=<UNDEF> (checkbox) [*<UNDEF>|rouge] RGB=<UNDEF> (checkbox) [*<UNDEF>|vert] RGB=<UNDEF> (checkbox) [*<UNDEF>|bleu] CYMK=<UNDEF> (radio) [cyan|jaune|magenta] FRUITS=<UNDEF> (option) [*<UNDEF>|pommes] FRUITS=<UNDEF> (option) [*<UNDEF>|poires] FRUITS=<UNDEF> (option) [*<UNDEF>|bananes] FRUITS=<UNDEF> (option) [*<UNDEF>|oranges] LEGUMES=brocolis (option) [*brocolis|carottes|courgettes] <NONAME>=Valider (submit)
Les *
correspondent aux valeurs « déjà vues » pour ce
champ par l'objet HTML::Form (explications ci-dessous).
Tout d'abord, il convient de noter que les champs checkbox
et
select
/multiple
sont traités différement des champs radio
et select
sans l'option multiple
. Cela vient du fait que les
premiers permettant de faire une selection multiple, ils peuvent
apparaître plusieurs fois dans la requête générée (par exemple
RGB=rouge&RGB=bleu&FRUITS=poires&FRUITS=oranges&LEGUMES=brocolis
)
et que le CGI recevant cette requête sait traiter une telle requête.
Nous pouvons commencer à essayer de modifier les valeurs des champs. Nous testerons ce qui sera renvoyé en analysant la requête générée à partir du formulaire modifié.
# valide le formulaire sans modification print $form->click()->content();
Ceci affiche :
LEGUMES=brocolis
Première remarque, les champs de type select
sans l'option multiple
ayant nécessairement une valeur sélectionnée, celle-ci est
toujours renvoyée, ce qui n'est pas le cas pour les champs de type
checkbox
ou les select
avec l'option multiple
.
Commençons par tester la boîte de sélection simple et les boutons radio :
# radio $form->value( CYMK => 'jaune' ); $form->value( CYMK => 'cyan' ); # select sans l'option multiple $form->value( LEGUMES => 'carottes' ); $form->value( LEGUMES => 'courgettes' ); # le résultat print $form->click()->content();
Le corps de la requête envoyée sera :
CYMK=cyan&LEGUMES=courgettes
Pour ces deux types de champs, comme il se doit, seule la dernière valeur sélectionnée est passée lors de l'envoi du formulaire.
Si on tente de sélectionner une valeur non proposée, cela provoquera une
erreur (la méthode value()
meurt en faisant appel à Carp::croak()
) :
# valeur illégale $form->value( CYMK => 'blanc' ); Illegal value 'blanc' at exemple.pl line 13
Note : Pour éviter que votre programme ne meure brutalement, vous
pouvez bien sûr utiliser le mécanisme d'exceptions associé à
eval { }
et traiter le message d'erreur contenu dans la variable
standard $@
(pour la gestion d'exceptions en Perl, je vous renvoie à
mon article de Linux Magazine 52, Les variables standard de Perl).
Voyons maintenant les champs avec des sélections multiples (checkbox
et select
avec l'option multiple
) :
# checkbox $form->value( RGB => 'rouge'); $form->value( RGB => 'bleu');
Ceci donne l'erreur Illegal value 'bleu'
. La valeur rouge
ne provoque
pas d'erreur, mais la valeur bleu
en provoque une ? Souvenez-vous
que la méthode find_input()
(utilisée par value()
) ne trouve
que le premier de tous les champs de même nom. Or dans le cas des
checkbox
(comme celui des select
/multiple
), HTML::Form crée
un objet de type HTML::Form::Input par valeur possible, ainsi que nous
l'avons vu dans le résultat de dump()
. Le premier champ
trouvé portant le nom RGB
n'a que deux valeurs possibles :
UNDEF
et rouge
. On peut le vérifier en utilisant la méthode
possible_values()
dans l'exemple suivant :
use Data::Dumper; # valeurs possibles de la première checkbox nommée RGB my $input = $form->find_input( 'RGB' ); print Dumper [ $input->possible_values() ];
Ce qui affiche :
$VAR1 = [ undef, 'rouge' ];
L'erreur rencontrée précédemment s'explique ainsi facilement. Il faudra donc sélectionner les cases à cocher d'une autre manière.
La méthode find_input()
accepte heureusement d'autres arguments que
le simple nom du champ. Le deuxième argument impose le type du champ
à trouver (au cas où plusieurs champs différents porteraient le même nom)
et le troisième permet d'imposer le numéro d'ordre du champ à trouver
(en comptant à partir de 1). On peut ainsi sélectionner les cases voulues,
pourvu qu'on connaisse leur ordre d'apparition dans le formulaire :
# checkbox $form->find_input( 'RGB', undef, 1 )->value(undef); $form->find_input( 'RGB', undef, 2 )->value('vert'); $form->find_input( 'RGB', undef, 3 )->value('bleu');
Ce qui coche les cases vert
et bleu
, tout en décochant la case
rouge
:
RGB=vert&RGB=bleu&LEGUMES=brocolis
Une autre possibilité est d'obtenir la liste des champs à l'aide de
inputs()
et de boucler dessus pour les modifier. Nous allons également
utiliser la méthode possible_values()
, qui (comme son nom l'indique)
liste l'ensemble des valeurs possibles pour ce champ. Si le champ n'a
pas un nombre limité de valeurs possibles, cette méthode retourne une
liste vide.
Voici un exemple permettant de sélectionner seulement les fruits
poires
et bananes
:
# récupère les champs nommés FRUITS for my $input ( grep { defined $_->name && $_->name eq 'FRUITS' } $form->inputs ) { # valide ceux qui acceptent 'poires' ou 'bananes' comme valeur $input->value($_) for grep { defined && /poires|bananes/ } $input->possible_values(); }
En général, utiliser find_input()
avec trois paramètres est un peu
plus compréhensible. Cela nécessite juste d'être certain de la présentation
du formulaire, puisque le troisième paramètre dépend de l'ordre dans lequel
les balises <input>
sont insérées dans le code HTML.
À titre d'information, sachez qu'il existe également une méthode
other_possible_values()
qui liste l'ensemble des valeurs non encore
utilisées pour un champ. Il y a toujours au moins une valeur par défaut
qui a été prise, ainsi qu'on l'a vu plus haut avec le résultat de
dump()
(les valeurs marquées par *
).
# trouve le premier champ FRUITS my $input = $form->find_input('FRUITS'); print "Possibles : ", map( {"<$_>"} $input->possible_values() ), "\n"; print "Restantes : ", map( {"<$_>"} $input->other_possible_values() ), "\n"; # sélectionne une valeur $input->value('pommes'); print "Restantes : ", map( {"<$_>"} $input->other_possible_values() ), "\n";
L'exemple ci-dessus affichera (avec un message d'avertissement
à cause de l'utilisation d'undef
dans le premier map
) :
Possibles : <><pommes> Restantes : <pommes> Restantes :
Rien n'est affiché à la fin, car toutes les valeurs possibles
(undef
et pommes
) ont été sélectionnées au moins une fois.
Je ne présente pas la méthode try_others
, car je ne lui ai pas
encore trouvé d'utilité.
Au début de cet article, quatre méthodes vous ont été successivement présentées pour télécharger une page depuis un site web. Nous avons analysé le code écrit, mais pas la performance des requêtes successives.
Un test, réalisé avec le module Benchmark, en faisant 500 requêtes successives sur http://www.yahoo.fr/ a donné les résultats suivants :
Rate lwp lwpsimple iosocket socket lwp 30.7/s -- -73% -91% -96% lwpsimple 113/s 269% -- -65% -86% iosocket 327/s 965% 189% -- -58% socket 781/s 2447% 591% 139% --
LWP n'est donc clairement pas un bon outil de déni de service. ;-)
Attention cependant, les chiffres mesurés s'appuient sur la notion de temps
CPU consommé. Les performances réseau ne sont donc pas directement prises
en compte.
La lenteur de LWP est principalement due à son interface objet, qui rajoute un certain nombre de couches et d'appels de fonction au-dessus de la partie purement réseau. Le gain recherché quand on utilise LWP, ce n'est pas la performance réseau brute, mais la simplicité de programmation et la capacité d'automatisation. De toute façon, on est rarement à quelques secondes près quand on remplit un formulaire sur le web.
La méthode get()
de LWP::Simple a été écrite
spécialement pour être plus rapide ; c'est d'ailleurs ce qu'indique
Gisle Aas lui-même dans le message
posté sur la liste de diffusion libwww@perl.org
:
Only thing wrong was that it was too heavy and I wanted LWP::Simple::get() to be really lightweight and fast to load for the common http case.
Les différences entre les performances des modules Socket et IO::Socket ont exactement les mêmes origines. La différence est celle qu'il y a entre une connexion directe faisant appel aux fonction natives C et une connexion à travers une surcouche objet.
Au final, tout a un coût ; on paye en performance ce que l'on gagne en facilité et en rapidité de développement. LWP fait gagner énormément du point de vue du développement.
Il existe cependant une méthode pour améliorer les performances réseau de LWP : l'utilisation de HTTP/1.1. En effet, cette version du protocole permet de faire passer plusieurs requêtes HTTP dans la même connexion TCP, en utilisant le chunked encoding (encodage par morceaux).
LWP peut ainsi utiliser un « cache » de connexions vers différents
serveurs pour mettre à profit le gain offert par HTTP/1.1. Pour cela,
il suffit de déclarer votre objet LWP::UserAgent avec l'attribut
keep_alive
:
# conservera trois connexions TCP ouvertes en même temps my $ua = LWP::UserAgent->new( keep_alive => 3 );
Sur une série de requêtes vers le même serveur (condition nécessaire), j'ai pu observer un gain d'environ 20% en temps. En effet, avec ce système, il y a beaucoup moins de connexions TCP à établir pour faire le même nombre de requêtes HTTP.
L'évolution du réseau fait qu'HTTP est en passe de devenir le TCP/IP du XXIe siècle : utilisé partout et pour tout, y compris quand ce n'est pas le protocole le plus adapté. LWP est le navigateur qui vous évitera de cliquer pour accéder tout de même aux masses de données disponibles. Les meilleurs modules de la hiérarchie WWW sur CPAN vous donnent une interface simple vers des bases de données souvent accessibles uniquement via HTTP.
LWP est vraiment une superbe librairie, qui m'a permis par exemple d'écrire un script nommé connect-tunnel (disponible sur CPAN), qui gère des tunnels CONNECT à travers un proxy HTTP. Comme LWP supporte la méthode CONNECT et l'authentification HTTP, la toute première version de ce script a été codée en moins d'une heure.
De son côté, mon module HTTP::Proxy repose lourdement sur les modules LWP::UserAgent et HTTP::Daemon.
L'idée que j'ai voulu développer dans cette série d'articles est qu'il existe de nombreuses applications où l'on veut nous imposer une interface web pour interagir avec le système, alors que ce n'est pas l'interface la plus adaptée. Nous le verrons encore mieux dans la suite de cette série, où vous découvrirez comment envoyer des fichiers à un site web par la méthode POST, comment vous connecter à des sites par HTTPS, des exemples d'automatisation expliqués pas à pas et surtout le module WWW::Mechanize, qui simplifie encore l'écriture de scripts d'automatisation du web.
Le RFC de référence pour HTTP (successeur des RFC 1945 et 2068), qui définit l'intégralité du protocole en pas moins de 176 pages. LWP est programmé dans la volonté de respecter au mieux les standards définis dans les RFC.
Ce RFC annule et remplace le RFC 2068 et il est mis à jour par le RFC 2817 (Upgrading to TLS Within HTT1.1).
Le RFC concernant HTTP/1.0 est le RFC 1945.
Ce RFC décrit le fonctionnement du système des cookies et des
en-têtes HTTP dédiés à leur gestion (Set-Cookie
, Cookie
,
Set-Cookie2
et Cookie2
).
Le RFC 2965 s'appuie sur l'expérience acquise par l'implémentation et l'utilisation de l'ancien système de cookies défini par Netscape dans le RFC 2109, qu'il annule et remplace.
À titre d'information, il est intéressant de consulter le RFC 2964, Use of HTTP State Management, qui liste les utilisations correctes qui peuvent être faites de ces fameux cookies.
La documentation générale de LWP présente les fonctionnalités de la librairie, la philosophie générale qui a présidé à son développement et la liste des classes qu'elle contient.
La lecture de la documentation des autres modules de la librairie (LWP::Simple, LWP::UserAgent, URI, HTTP::Request, HTTP::Response, HTTP::Message, HTTP::Headers) est évidemment recommandée.
Ce livre de recette contient un certain nombre d'exemples qui permettent de bien commencer avec LWP. Sont également expliquées les méthodes pour accéder au web à travers un proxy ou pour s'authentifier sur un serveur.
Sean Burke, l'auteur entre autres de HTML::TreeBuilder, présente dans ce livre les approches utilisant LWP pour la récupération automatisée d'informations sur le web.
Perlmonks, le site des moines de Perl, est présenté dans mon article de Linux Magazine 53 sur la documentation de Perl.
Google propose des dizaines d'utilisations de sa base de données, dans des applications plus ou moins expérimentales : http://labs.google.com/.
Les API Google sont disponibles sur http://www.google.com/apis/. Le module WWW::Search::Google de Leon Brocard utilise justement ces API SOAP pour faire des recherches (ceci nécessite une clé de licence fournie par Google).
libwww@perl.org
La liste de diffusion libwww@perl.org
est une liste dédiée aux
questions concernant la librairie LWP et l'accès au web en général.
Les fils de discussion sont en général intéressants à suivre
et les réponses toujours pertinentes.
La citation de Gisle Aas dans cet article vient clore un fil au sujet de
LWP::Simple::get()
sur les plates-formes Win32. L'identifiant du message est :
<lrfziqitpo.fsf@caliper.activestate.com>
.
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. Son module WWW::Gazetteer::HeavensAbove utilise LWP pour palier aux limites de l'interface de recherche du site http://www.heavens-above.com/countries.asp lors de la recherche d'informations topographiques sur les villes du monde.
Merci à Estelle et aux membres du groupe de travail « articles » des Mongueurs de Perl pour leur relecture attentive.
Copyright © Les Mongueurs de Perl, 2001-2012
pour le site.
Les auteurs conservent le copyright de leurs articles.