Retrouvez cet article dans : Linux Magazine 77
Après avoir décrit notre boîte à outils et quelques robots simples dans Linux Magazine 75, nous allons dans cette deuxième partie automatiser des sites plus complexes. Ceux-ci vont simuler une navigation plus longue et manipuler en profondeur les formulaires envoyés par les serveurs ou les requêtes que nous leur renverrons.
Filtrer les enchérisseurs sur eBay
Un des mongueurs de Lyon est un habitué d’eBay : il vend et achète régulièrement des objets sur ce site de vente aux enchères.
Dans le monde des ventes aux enchères, il y a deux types d’enchérisseurs :
- Ceux qui achètent effectivement les objets pour lesquels ils enchérissent, et qui ont un profil positif ;
- Ceux qui ont tendance à se rétracter peu avant la fin d’une enchère ou même juste après. Ils sont peu fiables et pénibles pour le vendeur, car ils conduisent à l’échec de la vente. Ces derniers ont un profil négatif ou nul. (Les nouveaux venus sur eBay ont aussi un profil nul, ce qui joue un peu en leur défaveur au début.)
Lorsqu’il est dans le rôle du vendeur, notre mongueur ne souhaite donc pas voir participer ces enchérisseurs au profil négatif ou nul. Pour cela, il peut :
- Ajouter dans la description de l’objet en vente un message demandant aux profils nuls de contacter le vendeur avant d’enchérir, mais en pratique ces messages sont ignorés ;
- Aller consulter régulièrement les profils des enchérisseurs et annuler les enchères faites par ceux qui ont un profil négatif ou nul ;
- Déléguer à un robot cette tâche de consultation systématique et de filtrage des « profils nuls ».
Nous allons bien sûr construire le robot.
Identification sur le site
Pour accéder à ces enchères sur eBay, il faut tout d’abord s’identifier.
mech-dump nous montre les formulaires de la page d’ouverture de session :
$ mech-dump https://signin.ebay.fr/ws/eBayISAPI.dll?SignIn
POST https://scgi.ebay.fr/ws/eBayISAPI.dll?
RegisterEnterInfo&siteid=71&co_partnerid=2&UsingSSL=1 [RegisterEnterInfo]
MfcISAPICommand=RegisterEnterInfo (hidden readonly)
co_partnerId=2 (hidden readonly)
siteid=71 (hidden readonly)
ru= (hidden readonly)
bin=-1 (hidden readonly)
<NONAME>=S’inscrire > (submit)
POST https://signin.ebay.fr/ws/eBayISAPI.dll?
co_partnerid=2&siteid=71&UsingSSL=1 [SignInForm]
MfcISAPICommand=SignInWelcome (hidden readonly)
siteid=71 (hidden readonly)
co_partnerId=2 (hidden readonly)
UsingSSL=1 (hidden readonly)
ru= (hidden readonly)
pp= (hidden readonly)
pa1= (hidden readonly)
pa2= (hidden readonly)
pa3= (hidden readonly)
i1=-1 (hidden readonly)
pageType=-1 (hidden readonly)
rtmData= (hidden readonly)
userid= (text)
pass= (password)
<NONAME>=Ouvrir une session en mode sécurisé > (submit)
keepMeSignInOption=<UNDEF> (checkbox) [*<UNDEF>/off|1/
Je souhaite rester connecté sur cet ordinateur ;
je fermerai moi-même la session. Astuces sur la sécurité
de votre compte Assurez-vous que l’URL affichée
ci-dessus commence par https://signin.ebay.fr/]
Le premier formulaire concerne l’inscription initiale, avec ouverture de compte chez eBay. Celui qui nous intéresse est le second qui correspond à l’ouverture d’une session (ainsi que l’atteste la présence des champs userid et pass).
# récupération du formulaire
$m->get(‘https://signin.ebay.fr/ws/eBayISAPI.dll?SignIn’);
die ‘Échec de connexion : ‘ . $m->res->status_line()
unless $m->success();
# remplissage et validation du formulaire
$m->form_number(2);
$m->set_fields(
userid => $userid,
pass => $pass,
);
$m->submit();
# connexion réussie ?
die ‘Échec de validation du formulaire : ‘ .
$m->res()->status_line()
unless $m->success();
Nous allons utiliser la technique utilisée avec Skype (dans GLMF 75) pour nous assurer que l’identification est correcte. La ligne suivante nous affichera la liste des cookies reçus par notre robot :
$m->cookie_jar()->scan( sub { print «$_[1]\n» } );
En cas d’identification réussie, le robot reçoit les cookies dp1, ebay, nonsession, ns1, s et secure_ticket. En cas d’échec, on constate que le cookie secure_ticket n’est pas envoyé. On peut remarquer de plus que c’est le seul des cinq cookies qui est marqué comme secure, c’est-à-dire qu’il ne peut être envoyé au serveur qu’à travers une connexion sécurisée (i. e. en SSL).
Comme pour Skype dans l’article précédent, la vérification de la réussite de notre identification se fait de la façon suivante, en inspectant les cookies :
{
my $ok = 0;
$m->cookie_jar()->scan( sub { $ok++ if $_[1] eq ‘secure_ticket’ } );
die “Échec d’identification : informations d’ouverture de session invalides»
unless $ok;
}
Notre script sait désormais se connecter et vérifier que l’identification est réussie.
Liste des objets et enchérisseurs
Nous allons maintenant récupérer la liste des objets que nous avons mis en vente, afin de pouvoir ensuite trouver les enchérisseurs dont le profil ne nous revient pas.
Notre robot possédant les bons cookies, nous pouvons nous connecter directement à la page qui nous intéresse :
# récupération de «mes ventes»
$m->get( ‘http://my.ebay.fr/ws/eBayISAPI.dll?MyeBay&CurrentPage=MyeBaySelling’);
die ‘Échec de connexion à «mes ventes» : ‘ .
$m->res()->status_line()
unless $m->success();
Les liens vers les pages des objets mis en vente sont reconnaissables à l’URL du lien associé : chacun contient la chaîne ViewItem et l’identifiant de l’objet sous la forme item=numero.
WWW::Mechanize fournit la méthode find_all_links(), qui renvoie la liste des liens correspondant aux paramètres de recherche fournis. Ces paramètres sont les mêmes pour les trois méthodes follow_link() (équivalent de get() à partir d’un lien dans la page), find_link() (qui renvoie un seul lien correspondant aux critères de recherche) et find_all_links() (qui renvoie tous les liens correspondants).
Les critères de recherche décrits ci-après ont tous (sauf le dernier, n) un équivalent de la forme xxx_regex qui accepte une expression régulière comme argument, afin de faire une recherche plus souple. Les paramètres sans le suffixe _regex ne correspondront qu’en cas d’identité stricte.
Ces critères sont :
text: Le texte du lien (contenu entre<a>et</a>) ;url: L’URL du lien (telle que codée dans la page, relative ou absolue) ;url_abs: L’URL de lien (convertie en URL absolue, même si elle est relative dans le fichier HTML) ;name: Le nom du lien (attribut name) ;tag: La balise du lien (plus utile sous la forme tag_regex pour obtenir des liens extraits de plusieurs balises).
Les liens sont extraits des balises <a href=...>, <area href=...>, <frame src=...>, <iframe src=...> et <meta content=...>.
n: Le numéro d’ordre du lien dans la page (WWW::Mechanizecompte à partir de 1). Pour les méthodesfollow_link()etfind_link()(qui ne doivent trouver qu’un lien au maximum), sinn’est pas fourni, il prend la valeur 1.
Nous allons ici utiliser la méthode find_all_links() avec le paramètre url_regex pour trouver les pages associées à tous les objets mis en vente :
my @ventes = $m->find_all_links( url_regex => qr/\?ViewItem.*&item=\d+/ );
Les objets renvoyés par find_all_links() sont de type WWW::Mechanize::Link. Ils disposent de plusieurs accesseurs comme url(), text(), tag(), etc. qui fournissent tous les informations concernant le lien tel qu’il était dans la page web. Ils peuvent être passés directement à la méthode get() de WWW::Mechanize qui saura les utiliser pour récupérer les pages concernées.
Récupération des profils négatifs ou nuls
En accédant à la page dédiée à chaque objet mis en vente, nous obtenons la liste des enchérisseurs. Soit $item l’élément de @ventes que nous analysons. Nous allons tout d’abord récupérer l’historique des enchères sur cet objet :
# boucle sur le contenu de @ventes
my $id = ( $_->url() =~ /item=(\d+)/g );
print “Objet : “, $item->text(), “ ($id)\n”;
# pour info
# connexion directe à l’historique des enchères sur cet objet
$m->get( ‘http://offer.ebay.fr/ws/eBayISAPI.dll?ViewBids&item=$id’ );
do {
warn “Échec de connexion aux enchères de l’objet $id : «
. $m->res()->status_line();
$m->back();
next;
}
unless $m->success();
# boucle sur le contenu de @ventes
my $id = ( $_->url() =~ /item=(\d+)/g );
print “Objet : “, $item->text(), “ ($id)\n”;
# pour info
# connexion directe à l’historique des enchères sur cet objet
$m->get( ‘http://offer.ebay.fr/ws/eBayISAPI.dll?ViewBids&item=$id’ );
do {
warn “Échec de connexion aux enchères de l’objet $id : «
. $m->res()->status_line();
$m->back();
next;
}
unless $m->success();
NOTE :
La méthode back() réalise la même opération que le bouton « Précédent » de votre navigateur web. WWW::Mechanize conserve en mémoire l’historique des pages visitées (et s’en sert par exemple pour renseigner le champ d’en-tête Referer:). Il est intéressant de suivre autant que possible la logique de la navigation et donc d’utiliser back() quand votre programme retourne en arrière (par exemple en fin de boucle). Ceci permet d’utiliser les méthodes usuelles de WWW::Mechanize dans presque tous les cas, et d’avoir des en-têtes Referer: cohérents avec l’organisation du site.
Les enchérisseurs sont listés sur cette page, avec un lien pour chaque profil. find_all_links() et une requête un peu plus élaborée que précédemment vont nous renvoyer la liste des liens vers des profils négatifs ou nuls.
# détection des enchérisseurs avec profil négatif ou nul
my @negs = $m->find_all_links(
# lien vers les détails du profil
url_regex => qr/\?ViewFeedBack/,
# profil négatif ou nul
text_regex => qr/^[-0]/
);
Il nous faut maintenant extraire le pseudonyme de ces enchérisseurs. Le lien ViewFeedBack contient un paramètre userid, ce qui va nous faciliter la chose.
Nous allons stocker les identifiants des utilisateurs dans une table de hachage, qui nous garantit par construction que chaque pseudonyme n’apparaîtra qu’une seule fois.
NOTE :
L’utilisation d’un hash pour gérer des listes non ordonnées dont les éléments n’apparaissent qu’une seule fois est un idiome Perl.
Normalement, l’expression régulière /userid=(.+?)(?:&|$)/ devrait suffire (on s’arrête en fin de chaîne ou au premier & rencontré).
my %negs;
foreach my $link (@negs) {
$link->url() =~
/(?:userid|ReturnUserEmail&requested)=(.+?)(?:&|$)/;
# ajoute la clé correspondant au
# pseudonyme dans le hash
$negs{$1} = ‘’;
}
Lors de nos essais, nous avons constaté que l’identifiant de l’utilisateur apparaît parfois après la chaîne ReturnUserEmail&requested= pour une raison obscure.
C’est pourquoi nous utilisons une expression un peu plus compliquée, afin de gérer tous les cas connus. Celle-ci pourra d’ailleurs être étendue si d’autres formes venaient à se présenter.
Annulation des enchères
Nous obtenons finalement un hash dont les clés sont les pseudos de tous les enchérisseurs importuns. Il ne reste plus qu’à accéder à la page d’annulation des enchères pour chacun d’entre eux et à soumettre le formulaire adéquat complété avec le numéro d’objet, le pseudo de l’acheteur et un petit message mentionnant la raison de l’éviction.
Nos manières expéditives ne doivent pas nous empêcher de rester polis.
# suppression des enchères non souhaitées
foreach my $pseudo (keys %negs) {
$m->get(‘http://offer.ebay.fr/ws/eBayISAPI.
dll?CancelBidShow’);
$m->submit_form(
# le formulaire 1 correspond à la boîte de recherche
# c’est donc le formulaire 2 que nous devons remplir
form_number => 2,
fields => {
item => $item,
buyeruserid => $pseudo,
info => “Profil <= 0 ; n’a pas “.
“pris contact avant d’enchérir.»
}
);
}
Le script complet
Tous les bouts de code présentés précédemment ont été mis ensemble pour produire un script complet qui accepte des options de ligne de commande (--userid, --pass, --verbose, etc.). Il ne reste plus qu’à ajouter la lecture d’un fichier $HOME/.ebayrc et il sera parfait (passer un mot de passe sur la ligne de commande, ce n’est pas beaucoup mieux que de le mettre dans le source du script : l’idéal est bien d’avoir un fichier de configuration à part).
Le script remanié reste d’une longueur raisonnable :
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use WWW::Mechanize;
# options par défaut
my %CONF = (
verbose => 0,
base => ‘ebay.fr’,
# À compléter avec vos identifiant
userid => ‘’,
# et mot de passe
pass => ‘’,
);
# récupération des paramètres de ligne de commande
GetOptions( \%CONF, «verbose!», «base=s», «userid=s», «pass=s» )
or die << ‘USAGE’;
Options disponibles :
--base <site> : base site (ebay.fr, ebay.com, etc.)
--userid <userid>
--pass <password>
--verbose
USAGE
# calcul des URL spécifiques
my %url = (
signin => «http://signin.$CONF{base}/ws/eBayISAPI.dll?SignIn»,
sales =>
«http://my.$CONF{base}/ws/eBayISAPI.dll?».
«MyeBay&CurrentPage=MyeBaySelling»,
bids => «http://offer.$CONF{base}/ws/».
«eBayISAPI.dll?ViewBids&item=»,
cancelbid => «http://offer.$CONF{base}/ws/».
«eBayISAPI.dll?CancelBidShow»
);
# création du robot
my $m = WWW::Mechanize->new();
# récupération du formulaire
$m->get( $url{signin} );
die «Échec de connexion à la page de login : « .
$m->res()->status_line()
unless $m->success();
# remplissage et validation
$m->form_number(2);
$m->set_fields(
userid => $CONF{userid},
pass => $CONF{pass},
);
$m->submit();
die “Échec d’envoi du formulaire : « .
$m->res()->status_line()
unless $m->success();
# identification réussie ?
print $m->cookie_jar()->as_string();
{
my $ok = 0;
$m->cookie_jar()->scan( sub
{ $ok++ if $_[1] eq ‘secure_ticket’ }
);
die “Échec du login : informations d’ouverture «.
«de session invalides»
unless $ok;
print «Connecté à $CONF{base} en tant que $CONF{userid}\n»
if $CONF{verbose};
}
# récupèration de «mes ventes»
$m->get( $url{sales} );
die ‘Échec de connexion à «mes ventes» : ‘ . $m->res()->status_line()
unless $m->success();
my @ventes = $m->find_all_links
( url_regex => qr/\?ViewItem.*&item=\d+/ );
# traitement de chaque objet en vente
for my $item (@ventes) {
my $id = ( $_->url() =~ /item=(\d+)/g );
print «Objet : «, $item->text(), « ($id)\n» if $CONF{verbose};
# connexion directe à l’historique des enchères sur cet objet
$m->get( $url{bids} . $id );
do {
warn “Échec de connexion aux enchères de l’objet $id : «
. $m->res()->status_line();
$m->back();
next;
}
unless $m->success();
# détection des enchérisseurs avec profil négatif ou nul
my @negs = $m->find_all_links(
url_regex => qr/\?ViewFeedBack/,
text_regex => qr/^[-0]/,
);
my %negs;
foreach my $link (@negs) {
$link->url() =~
/(?:userid|ReturnUserEmail&requested)=(.+?)(?:&|$)/;
# ajoute la clé correspondant au pseudonyme dans le hash
$negs{$1} = ‘’;
}
# suppression des enchères non souhaitées
foreach my $pseudo ( keys %negs ) {
$m->get( $url{cancelbid} );
$m->submit_form(
form_number => 2,
fields => {
item => $item,
buyeruserid => $pseudo,
info =>
“Profil <= 0 ; n’a pas pris”.
“ contact avant d’enchérir.»
}
);
$m->back();
}
$m->back();
}
Un point important à noter est que, puisque la base d’utilisateurs eBay est mondiale et que tous les sites eBay utilisent le même moteur de gestion de comptes et d’enchères, les divers sites eBay sont complètement interchangeables du point de vue de notre navigation (à la langue du site près, d’où l’intérêt de s’intéresser aux éléments invariants entre les langues).
Ainsi, si l’on remplace ebay.fr par ebay.com, ebay.ca, ebay.de, ebay.at, ebay.ie, ebay.it, ebay.nl, ebay.es, ebay.ch, ebay.co.uk, ebay.com.au ou ebay.com.cn, l’identification (et donc très probablement le reste du script) fonctionne parfaitement.
Les sites ebay.se, ebay.com.hk, ebay.ph, ebay.com.sg semblent utiliser des moteurs différents (mais identiques entre eux).
Une exception notable est le site ebay.be qui semble partagé entre flamands et wallons : il y a un niveau d’indirection supplémentaire pour le choix de la langue qui empêche le script d’identification de fonctionner.
Quelques autres sites eBay ne fonctionnent pas si on utilise ce script tel quel. Je n’ai pas cherché à gérer cet aspect dans cet article, me contentant d’une option --base pour éventuellement changer de site eBay.
Déposer ses JPEG chez le photographe virtuel
Depuis que j’ai un appareil photo numérique, je fais trop de photos. Le format JPEG n’étant pas toujours adapté pour montrer les photos à la famille, il faut parfois sortir les photos sur papier.
Un certain nombre d’entreprises proposent de développer vos photos numériques très facilement : il suffit d’aller sur leur site web, d’y déposer les photos et d’indiquer dans quel magasin de l’enseigne on veut aller les chercher.
Ça marche bien pour une dizaine de photos ; mais c’est moins pratique quand je veux télécharger des centaines de photos de la plus belle nièce du monde... Je préférerais donc lister les photos dans un fichier que je passerais à un script qui se chargerait du reste.
C’est ce que nous allons faire.
J’ai choisi pour mes photos l’entreprise Photo Station (http://www.photostation.fr). Comme d’habitude, nous allons d’abord naviguer un peu sur leur site, afin de nous faire une idée de l’interface du site web et des formulaires que nous allons rencontrer.
Exploration du site

Fig. 1 : Choix de l’interface !
Première bonne surprise, le site propose une interface HTML en plus de l’interface ActiveX (réservée à Internet Explorer). C’est évidemment sur celle-ci que nous allons nous appuyer. La navigation sur le site par l’interface HTML se fait selon la progression suivante :
- 1. Sélection de l’interface (nous choisirons toujours l’interface HTML) ;
- 2. Sélection du type de travail (« Formats classiques », « Agrandissements », « Produits Photo Fun ») ;
- 3. Sélection de l’interface (ActiveX ou HTML) ;
- 4. Sélection et envoi des fichiers images ;
- 5. Sélection du format des photos ou sélection du format de chaque image (optionnel) ;
- 6. Si l’on ne s’était pas identifié jusqu’à présent (le formulaire apparaît sur toutes les pages), identification avec son compte utilisateur ;
- 7. Sélection du magasin et acceptation des conditions ;
- 8. Validation de la commande
À l’issue de cette visite, nous avons déjà constaté (rien qu’en regardant les URL dans le navigateur) que hormis la page d’accueil, l’intégralité de l’application d’envoi des photos est gérée par le site photoprintit.de.
Les « Produits Photo Fun » étant très spécifiques, nous allons nous intéresser uniquement aux « Formats classiques » et tirer toutes les photos au même format. Étant donnée une liste de photos, le rôle de notre script sera donc de les envoyer sur le site, de sélectionner le format des tirages, de sélectionner un magasin et enfin de valider la commande. Et bien sûr, ne pas oublier de nous identifier à un moment ou un autre.
Commençons par l’identification justement, ça ne doit pas être bien compliqué.
Identification sur le site
Comme tout bon client, nous allons créer manuellement un compte utilisateur avec notre adresse e-mail et le mot de passe idoine. Ceux-ci nous serviront à passer la commande à la fin.
Le formulaire d’identification est présent dès la page d’accueil de http://www.photostation.fr/. Pourtant, bien que visible sur celle-ci chargée par notre navigateur, le formulaire d’identification en question n’est pas détecté par mech-dump :
$ mech-dump http://www.photostation.fr/index.php
POST http://www.photostation.fr/entreprise/newsletter.php [newsletter]
E_MAIL=Votre e-mail (text)
imageField=<UNDEF> (image)
POST http://www.photostation.fr/magasin/viamichelin.php [list3]
intMapType=1 (hidden readonly)
productId=50739 (hidden readonly)
from=1234 (hidden readonly)
withCriteria=false (hidden readonly)
strCountry=000001424 (hidden readonly)
strCP= (text)
<NONAME>=<UNDEF> (image)
NOTE :
En période promotionnelle, on trouve parfois une animation Flash au bout de http://www.photostation.fr/. La « vraie » page principale se trouve alors à http://www.photostation.fr/index.php.
En fait, le formulaire de connexion est fourni dans une balise iframe. Ceci confirme d’ailleurs que PhotoStation utilise les services d’un fournisseur spécialisé pour la récupération des photos : photoprintit.de. WWW::Mechanize nous permet de suivre des liens à partir de l’indication de la balise qui les contient. Nous allons ainsi récupérer le contenu du cadre en question et afficher le formulaire :
$m->get(‘http://www.photostation.fr/index.php’);
$m->follow_link( tag => «iframe» );
print $m->current_form()->dump();
Ce qui nous affichera :
POST http://asp07.photoprintit.de/microsite/customers/
1156/live/templates/login_iframe.php [external_login_form]
PHPSESSID=184c6840f65e07d7057c0171dcbfe886 (hidden readonly)
external_login_user=Email (text)
external_login_pwd=Mot de passe (password)
external_login[]=<UNDEF> (image)
Remplir le formulaire est trivial :
$m->set_fields(
external_login_user => $email,
external_login_pwd => $password,
);
$m->submit();
Normalement, la page reçue en retour contient l’adresse email utilisée pour se connecter, confirmant que nous nous sommes correctement identifiés. On peut le vérifier en inspectant le contenu de $m->content(), comme ceci :
print $m->content =~ /\Q$email\E/ ? «ok» : «nok»;
NOTE :
L’utilisation de \Q et \E, qui émulent quotemeta() à l’intérieur de l’expression régulière, permet d’empêcher l’interprétation par l’expression régulière des points et autres métacaractères que l’adresse email pourrait contenir.
Le seul problème, c’est que notre petit script de test affiche nok.
Pour comprendre ce qui se passe, nous allons utiliser notre proxy et tenter de nous identifier avec Firefox :
POST http://asp09.photoprintit.de/microsite/customers/
1156/live/templates/login_iframe.php
Cookie: PHPSESSID=5b56e6e73c7a90624c0a2755248f250a
Referer: http://asp07.photoprintit.de/microsite/1156/customers/1156/
live/templates/login_iframe.php
PHPSESSID => 5b56e6e73c7a90624c0a2755248f250a
external_login_user => *******
external_login_pwd => *******
external_login[].x => 0
external_login[].y => 0
200 OK
Content-Type: text/html; charset=utf-8
Et comparer avec notre robot :
POST http://asp06.photoprintit.de/microsite/customers/
1156/live/templates/login_iframe.php
Cookie: PHPSESSID=bbf50583177dc9c43cc02dc39af4f397
Cookie2: $Version=»1»
Referer: http://asp.photoprintit.de/1156/customers/
1156/live/templates/login_iframe.php
PHPSESSID => bbf50583177dc9c43cc02dc39af4f397
external_login_user => *******
external_login_pwd => *******
200 OK
Content-Type: text/html; charset=utf-8
Outre l’en-tête Cookie2, la différence principale c’est qu’avec le navigateur, nous avons cliqué sur l’image contenue dans le formulaire. Notre robot n’a pas renvoyé les champs external_login[].x et external_login[].y.
Heureusement, WWW::Mechanize permet également de simuler le clic sur une image :
$m->click(‘external_login[]’);
En utilisant cette commande au lieu de submit(), le robot envoie les champs attendus et notre script de test affiche ok, confirmant que nous avons réussi à nous authentifier sur le site !
Récupération du formulaire d’envoi des photos
Maintenant que nous sommes identifiés, nous allons déposer nos photos. Pour cela, il suffit de sélectionner l’interface « Netscape/Autre » sur la page principale.
Avec la méthode back(), nous allons revenir à la page principale (le robot est actuellement sur la page de réponse au formulaire d’identification).
# après identification
$m->back(); # retour au formulaire d’identification
$m->back(); # retour à la page principale
# on suit le lien vers l’interface HTML
$m->follow_link( url_regex => qr/htmlclient/ );
# affiche tous les formulaires de la page
print $_->dump(), «\n» for $m->forms();
Voici les formulaires visibles :
POST http://asp05.photoprintit.de/microsite/htmlclient.php?
customerid=1156 [external_login_form]
PHPSESSID=4a58ab74f9342a384a60e57c591e581d (hidden readonly)
accountmode=1 (hidden readonly)
external_login_user=Email (text)
external_login_pwd=Mot de passe (password)
external_login[]=<UNDEF> (image)
POST http://asp05.photoprintit.de/microsite/1156/htmlclient.php
PHPSESSID=4a58ab74f9342a384a60e57c591e581d (hidden readonly)
quantity=10 (option) [*10|20|30|40|50]
<NONAME>=<UNDEF> (image)
POST http://asp05.photoprintit.de/microsite/1156/receive.php
(multipart/form-data) [upload_form]
PHPSESSID=4a58ab74f9342a384a60e57c591e581d (hidden readonly)
UPLOAD_METER_ID=85582e6a2b60e80cc5339f7d809ff308 (hidden readonly)
file0= (file)
file1= (file)
file2= (file)
file3= (file)
file4= (file)
file5= (file)
file6= (file)
file7= (file)
file8= (file)
file9= (file)
upload=<UNDEF> (image)
Avant de nous occuper du dernier formulaire, il nous faut remarquer un petit problème : le premier formulaire de la page est un formulaire d’identification, alors que nous sommes censés nous être identifiés précédemment. Que s’est-il passé ?
La réponse à cette question apparaît quand on demande au robot de nous afficher ses cookies :
print $m->cookie_jar()->as_string();
Ce qui donne le résultat suivant :
Set-Cookie3: PHPSESSID=4a58ab74f9342a384a60e57c591e581d;
path=»/»; domain=asp05.photoprintit.de; path_spec; discard; version=0
Set-Cookie3: PHPSESSID=bbf50583177dc9c43cc02dc39af4f397;
path=»/»; domain=asp06.photoprintit.de; path_spec; discard; version=0
Nous avons deux cookies, l’un pour asp05.photoprintit.de et l’autre pour asp06.photoprintit.de.
En y regardant d’un peu plus près, on constate qu’effectivement, le formulaire d’identification a été récupéré sur asp06 et le formulaire d’envoi des photos sur asp05. Selon toute vraisemblance, il y a un système d’équilibrage de charge qui nous a redirigé une première fois vers asp06 et la seconde vers asp05. Seul le cookie associé à asp06 nous identifie.
Après vérification, ce problème se pose également avec Firefox quand on s’identifie dans le <iframe> affiché sur la page principale. Quand ensuite, on choisit une commande de photos, la boîte d’identification attend encore notre e-mail et notre mot de passe.
La raison est très simple : le site vers lequel pointe la première page a un URL en asp.photoprintit.de. Quand on clique effectivement sur le lien, le site fait une redirection automatique vers l’un des sites asp01 à asp10. Notre proxy espion nous montre comment cela se passe :
GET http://asp.photoprintit.de/
302 Found
Content-Type: text/html; charset=iso-8859-1
Location: http://asp03.photoprintit.de/
Une fois connecté sur le site, tous les liens sont relatifs et on reste donc sur le même serveur.
Or l’iframe qui se trouve dans la page d’accueil du site PhotoStation pointe également vers le nom générique asp (vers http://asp.photoprintit.de/1156/customers/1156/live/templates/login_iframe.php, pour être précis). Le jeu des redirections fait donc qu’il est fort peu probable qu’une identification réalisée dans le formulaire de l’iframe du début soit valable sur le serveur qui traitera notre demande (1 chance sur 10 très exactement).
Heureusement, dès que l’on commence à aller chercher les pages sur photoprintit.de, le formulaire d’identification fait partie intégrante de page (ce n’est plus un iframe). Il nous suffit donc de remettre les lignes de notre script dans le bon ordre et d’aller d’abord sur le site du prestataire, puis de nous identifier.
my $m = WWW::Mechanize->new;
$m->get(‘http://www.photostation.fr/’);
# d’abord on va chez le fournisseur
$m->follow_link( url_regex => qr/htmlclient/ );
# puis on s’identifie
$m->set_fields(
external_login_user => $email,
external_login_pwd => $password,
);
$m->click( ‘external_login[]’ );
print $_->dump(), «\n» for $m->forms();
print $m->cookie_jar()->as_string();
Ceci a l’avantage de simplifier notre script : comme le formulaire d’identification fait partie de la page, il n’y a plus besoin de faire des back() pour revenir à la page qui nous intéresse.
Voici le résultat :
POST http://asp07.photoprintit.de/microsite/htmlclient.php
external_logout[]=<UNDEF> (image)
POST http://asp07.photoprintit.de/microsite/htmlclient.php
quantity=10 (option) [*10|20|30|40|50]
<NONAME>=<UNDEF> (image)
POST http://asp01.photoprintit.de/microsite/receive.php
(multipart/form-data) [upload_form]
UPLOAD_METER_ID=22a21519eca86d05620e69e709f7945d (hidden readonly)
file0= (file)
file1= (file)
file2= (file)
file3= (file)
file4= (file)
file5= (file)
file6= (file)
file7= (file)
file8= (file)
file9= (file)
upload=<UNDEF> (image)
Set-Cookie3: PHPSESSID=9d08c5f4bf02846ae8580c6642937e35;
path=»/»; domain=asp07.photoprintit.de; path_spec; discard; version=0
Le formulaire d’identification nous propose cette fois de nous déconnecter (prouvant ainsi que nous sommes identifiés), un formulaire permet de requérir plus de champs d’ajout de photo, et le dernier formulaire est celui qui permet de déposer les photos.
Enfin, nous n’avons cette fois-ci qu’un seul cookie, associé au serveur qui va gérer toute notre transaction.
Il faut noter que le formulaire d’identification/déconnexion sera toujours le premier formulaire des pages à venir.
Les formulaires que nous validerons désormais seront toujours le deuxième ou le troisième de la page.
Remplissage et validation du formulaire d’envoi des photos
Par défaut, le formulaire envoyé par le site propose 10 entrées pour des fichiers de photo, et un formulaire permet de sélectionner 20, 30, 40 ou 50 entrées.
Si on veut envoyer plus de 10 photos, il faudra donc faire une requête supplémentaire pour charger le formulaire adéquat.

Fig. 2 : Formulaire d’envoi des images
Ce formulaire est le deuxième de ceux renvoyés par la requête précédente :
POST http://asp07.photoprintit.de/microsite/htmlclient.php
quantity=10 (option) [*10|20|30|40|50]
<NONAME>=<UNDEF> (image)
En le regardant, on imagine sans peine que si on change la valeur du champ quantity pour y mettre une valeur quelconque, le serveur nous renverra une page avec le nombre de champs file demandé.
$m->form_number(2); # il s’agit du deuxième formulaire de la page
$m->field( quantity => 7 ); # valeur non standard
On constate que HTML::Form respecte scrupuleusement les spécifications du formulaire et n’accepte pas de valeur non prévue. Notre script meurt en affichant le message suivant :
Illegal value ‘7’ for field ‘quantity’ at /usr/share/perl5/WWW/Mechanize.pm line 1030
Cela vient du fait que le champ quantity est défini comme étant de type <select> dans le source HTML du formulaire.
Dans l’objet HTML::Form construit par WWW::Mechanize à partir du source HTML, cela se traduit par un objet de type HTML::Form::ListInput. C’est cet objet qui fait les vérifications et provoque l’erreur ci-dessus.
Qu’à cela ne tienne, nous n’avons qu’à décider qu’il s’agit en fait d’un champ de type text normal !
Pour cela, il suffit de changer l’idée que Perl se fait de cet objet, en changeant sa classe :
bless $m->current_form()->find_input(‘quantity’), ‘HTML::Form::TextInput’;
Le champ quantity acceptera désormais n’importe quelle valeur et l’enverra au serveur lors de la validation du formulaire.
Soit @photos le tableau contenant la liste des fichiers à envoyer. Nous pourrons donc étendre le formulaire si le besoin s’en fait sentir :
if ( @photos > 10 ) {
$m->form_number(2);
bless $m->current_form()->find_input(‘quantity’),
‘HTML::Form::TextInput’;
$m->field( quantity => scalar @photos );
$m->click;
}
Notre script pourra donc envoyer autant d’images que nous le souhaitons via un seul formulaire.
Nous pouvons maintenant remplir ce formulaire sans difficulté :
# sélectionne le 3ème formulaire de la page
$m->form_number(3);
# ajoute les photos au formulaire
$m->set_fields( «file$_» => $photos[$_] ) for 0 .. @photos - 1;
Indicateur de progression et optimisation de la mémoire
Une photo d’assez bonne qualité pour être imprimée pèse entre 1,5 et 2 mégaoctets. Même pour une dizaine de photos, ça veut dire envoyer pas loin 16 Mo en remontant à travers la liaison ADSL.

Fig. 3 : La fenêtre annexe de téléchargement
Pendant le téléchargement des photos, le site envoie une popup (associée à un identifiant unique) rafraîchie régulièrement qui donne l’état d’avancement du transfert.
Dans notre script, si nous appelons directement la méthode submit(), le script ne récupèrera la main qu’à la fin du téléchargement c’est-à-dire au bout d’une dizaine de minutes sur une ligne à 30 ko/s. Il nous faut donc donner la possibilité au script de nous indiquer son avancement, sans quoi il nous sera impossible de savoir ce qui se passe et on risque de l’interrompre à tort. De plus, nous ne voulons pas charger 16 Mo de photos en mémoire juste pour les envoyer sur le réseau !
Heureusement, la libraire LWP sait justement aussi produire des requêtes HTTP::Request qui envoient beaucoup de données au serveur sans tout charger en mémoire (merci Gisles !). Et WWW::Mechanize, en bonne sous-classe de LWP::UserAgent, sait les utiliser.
Habituellement, un objet HTTP::Request associé à un POST contient un paramètre accessible par la méthode content(). Ceci permet d’avoir accès (en lecture et en écriture) au contenu du corps de la requête avant de l’envoyer. Pour les requêtes volumineuses, il est prévu que cet attribut puisse être une fonction de rappel qui renvoie les données morceau par morceau. Ainsi, la mémoire occupée reste raisonnable.
HTTP::Request::Common, la classe de base de HTTP::Request sait construire automatiquement la fonction en question quand on veut envoyer des fichiers au serveur. Il suffit pour cela de mettre la variable globale HTTP::Request::Common::DYNAMIC_FILE_UPLOAD à une valeur vraie.
C’est la méthode make_request() de HTML::Form qui produit l’objet HTTP::Request attendu.
my $req;
{
no warnings;
local $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
$req = $m->current_form()->make_request;
}
NOTE :
no warnings évite que l’avertissement Name “HTTP::Request::Common::DYNAMIC_FILE_UPLOAD” used only once: possible typo soit affiché.
Ainsi, pour réaliser un petit affichage qui va nous faire patienter pendant le téléchargement, il suffit de remplacer la routine en question par une autre qui réalise un affichage. Cette nouvelle routine devra évidemment renvoyer les données initialement renvoyées par celle générée par HTTP::Request::Common, afin de ne pas modifier la requête.
Nous allons ici nous contenter d’afficher un pourcentage d’avancement. Pour cela, nous utilisons la longueur totale de la requête telle que calculée par HTTP::Request::Common (et accessible depuis l’objet HTTP::Request via la méthode content_length()), et nous calculons la taille des données déjà envoyées grâce à une petite addition. Notre code devient :
my $req;
{
no warnings;
local HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
$req = $m->current_form()->make_request();
# récupère la routine générée par HTTP::Request::Common
my $sub = $req->content();
my $done = 0;
my $total = $req->content_length();
$req->content(
sub {
my $data = $sub->(); # appelle la routine générée
return unless defined $data;
$done += length $data;
printf «\rTransfert: %2d%%», $done / $total * 100;
return $data;
}
);
}
NOTE :
Le no warnings évite en plus ici l’affichage d’un avertissement Use of uninitialized value in length quand $data vaut undef en fin de lecture des fichiers.
Pour que l’affichage se fasse correctement (nous utilisons \r), il ne faut pas oublier de passer la sortie standard en mode non tamponné (unbuffered). La méthode la plus lisible pour le faire est :
use IO::Handle;
STDOUT->autoflush(1);
Il ne reste plus qu’à passer la requête à WWW::Mechanize pour qu’il envoie effectivement les photos et qu’on puisse passer à la suite du traitement.
Sélection du format des photos
Dans le cadre de cet article, nous nous intéressons seulement à la commande d’un lot de photos toutes dans le même format. La page suivante nous propose donc le formulaire suivant pour sélectionner le format de nos tirages :
POST http://asp07.photoprintit.de/microsite/1156/formats.php
orderdata[4]=0 (text)
orderdata[5]=1 (text)
orderdata[11]=0 (text)
orderdata[6]=0 (text)
album=0 (hidden readonly)
album=<UNDEF> (checkbox) [*<UNDEF>/off|1/ ]
order[]=<UNDEF> (image)
customcopies=1 (text)
customformatid_4[]=4 (image)
customformatid_5[]=5 (image)
customformatid_11[]=11 (image)
customformatid_6[]=6 (image)
custom[]=<UNDEF> (image)

Fig. 4 : Sélection du format
En comparant avec le formulaire visualisé par notre navigateur, nous voyons que pour le cas qui nous occupe (commande express de photos toutes au même format), seuls les quatre premiers champs nous intéressent.
La valeur dans le champ correspond au nombre de tirages de chaque taille qui sera fait pour chaque photo.
En ce qui concerne les formats, on vérifie avec le source HTML que le champ orderdata[4] correspond au format 9x11, orderdata[5] au format 10x13, orderdata[11] au format 11x15 et orderdata[6] au 13x17.
Pour connaître le nombre total de photos, il faut donc multiplier la somme des valeurs de ces champs par le nombre total de photos.
Une fois le format sélectionné, on clique sur le bouton « Commande Express » qui correspondant au champ order[] du formulaire.
À noter que même si on prend le choix par défaut, il est préférable de le sélectionner nous-même, pour le cas où le défaut changerait dans le futur (évitons les mauvaises surprises).
$m->form_number(2);
$m->set_fields(
‘orderdata[4]’ => 0, # 9x11
‘orderdata[5]’ => 1, # 10x13
‘orderdata[11]’ => 0, # 11x15
‘orderdata[6]’ => 0, # 13x17
);
$m->click(‘order[]’);
Choix du mode de livraison
Une fois le format validé, il reste à choisir le mode de livraison.
Afin de ne pas nous embarquer dans la gestion de paiement en ligne (et pour nous éviter des frais supplémentaires), nous sélectionnons la « livraison en magasin ».

Fig.5 : Choix du mode de livraison
C’est le formulaire suivant qui nous le propose :
POST http://asp07.photoprintit.de/microsite/1156/cart.php [cartform]
transfer_type=FC (radio) [FM/
Livraison à domicile (Payant)|*FC/ Livraison en magasin (Gratuit)]
ok[]=<UNDEF> (image)
startorder[]=<UNDEF> (image)
couponid= (text)
ok=<UNDEF> (image)
Nous maîtrisons complètement l’opération, désormais :
$m->form_number(2);
$m->field( transfer_type => ‘FC’ ); # Livraison en magasin
$m->click(‘startorder[]’); # bouton “Confirmer ma commande”
Nous sommes presque au bout de nos peines.
Sélection du magasin où les photos seront livrées
Pour pouvoir aller récupérer nos photos une fois développées, nous devons choisir dans quel magasin nous irons.
Ce nouvel écran nous propose une recherche du magasin par code postal ou ville ou la sélection directe par une liste déroulante.

Fig.6 : Formulaire de sélection du magasin et validation
POST https://asp07.photoprintit.de/microsite/confirm.php [payment]
search_zip= (text)
search_city= (text)
search[]=<UNDEF> (image)
loc_id= (hidden readonly)
loc_id= (option) [*/SELECTIONNEZ
VOTRE MAGASIN CI-DESSOUS|29733/PHOTO STATION, 18 Rue Du
Mal Joffre, 01000 Bourg en Bresse|29734/PHOTO STATION,
C.Cial Du Fayet, 02100 Saint-Quentin|...
accept=<UNDEF> (checkbox) [*<UNDEF>/off|on/
J’accepte les conditions générales de vente. ]
startorder[]=<UNDEF> (image)
Dans le cas de notre script, nous avons juste besoin de connaître l’identifiant numérique du magasin qui nous intéresse. On peut l’obtenir simplement à partir des données dans le formulaire.
# trouve le champ de HTML::Form qui contient la liste des magasins
$m->form_number(2);
my $input = $m->current_form()->find_input( ‘loc_id’, ‘option’ );
# utilisation d’une tranche de hash
my %cities;
@cities{ $input->possible_values } = $input->value_names;
# affichage de la liste, par ordre des identifiants
print «$_\t$cities{$_}\n» for sort keys %cities;
Il suffit de sauver la sortie du script dans un fichier. C’est ainsi que je trouve la boutique la plus proche de chez moi dans la liste des 288 magasins :
...
29620 PHOTO STATION, 25 Rue Des Boulangers, 68000 Colmar
29621 PHOTO STATION, 53 Rue Du Sauvage, 68100 Mulhouse
29622 PHOTO STATION, 47 Rue Victor Hugo, 69002 Lyon
29623 PHOTO STATION, 45 Rue De La Republique, 69002 Lyon
29624 PHOTO STATION, Centre Cial Part Dieu, 69003 Lyon
...
Dans le script final présenté en fin d’article, une option a été ajoutée pour aller uniquement chercher la liste des magasins sur le site, en envoyant une petite image et en coupant la transaction avant la confirmation finale.
Validation
Jusqu’à présent quand le script s’arrêtait, la commande n’était pas validée, et les photos restaient donc sur un coin de disque dur sur le serveur (en attendant qu’une tâche planifiée ne les efface).
Cette fois-ci, nous sommes identifiés. Il y a des photos et, si on valide, il faudra aller les chercher chez le marchand. :-) Avant de valider pour nos tests finaux, nous allons donc choisir des photos que nous voulons vraiment développer, et pas juste de petits fichiers images pour économiser la bande passante (et accélérer les tests). J’ai choisi pour ce test une photo de la plus belle nièce du monde. ;-)
Arrivé à la page de confirmation, il faut cocher la case « J’accepte les conditions générales de vente. » et cliquer sur le bouton « Confirmer ma commande ».
La case à cocher correspond au champ HTML suivant :
<td><input type=»checkbox» name=»accept»></td>
La méthode de WWW::Mechanize pour cocher les cases s’appelle tick() et nécessite de connaître la valeur associée à chaque case à cocher. Ici, comme on peut le voir, il n’y a qu’une case avec aucune valeur associée. Heureusement, le dump du formulaire nous a permis de voir quelle valeur HTML::Form associe par défaut à une balise <input> de type checkbox sans paramètre value :
accept=<UNDEF> (checkbox) [*<UNDEF>/off|on/|
J’accepte les conditions générales de vente.| ]
Nous devrons donc cocher la case on.
$m->field( loc_id => $magasin );
$m->tick(‘accept’, ‘on’);
$m->click(‘startorder[]’);
Avant de cocher automatiquement la case accept, je vous engage au moins à survoler les conditions générales de vente (disponibles à l’adresse http://asp.photoprintit.de/microsite/1156/terms.php). Celles-ci indiquent en effet que vous renoncez à votre droit de rétraction, pour la simple raison que les produits seront déjà fabriqués (et livrés) avant la fin du droit de rétractation. Autrement dit, si vous faites des tests qui vont jusqu’à la confirmation finale, prévoyez comme moi d’aller chercher les photos. :-)
<td align=»right» colspan=»3»>
<span class=»error»>Veuillez sélectionner
un magasin.</span><br><br>
</td>
</tr>
<tr>
<td><input type=»checkbox» name=»accept» checked></td>
Après validation, nous recevons une page qui contient le texte suivant :
<td><input type=»checkbox» name=»accept»></td>
La méthode de WWW::Mechanize pour cocher les cases s’appelle tick() et nécessite de connaître la valeur associée à chaque case à cocher. Ici, comme on peut le voir, il n’y a qu’une case avec aucune valeur associée. Heureusement, le dump du formulaire nous a permis de voir quelle valeur HTML::Form associe par défaut à une balise <input> de type checkbox sans paramètre value :
accept=<UNDEF> (checkbox) [*<UNDEF>/off|on/|
J’accepte les conditions générales de vente.| ]
Nous devrons donc cocher la case on.
$m->field( loc_id => $magasin );
$m->tick(‘accept’, ‘on’);
$m->click(‘startorder[]’);
Avant de cocher automatiquement la case accept, je vous engage au moins à survoler les conditions générales de vente (disponibles à l’adresse http://asp.photoprintit.de/microsite/1156/terms.php). Celles-ci indiquent en effet que vous renoncez à votre droit de rétraction, pour la simple raison que les produits seront déjà fabriqués (et livrés) avant la fin du droit de rétractation. Autrement dit, si vous faites des tests qui vont jusqu’à la confirmation finale, prévoyez comme moi d’aller chercher les photos. :-)
Après validation, nous recevons une page qui contient le texte suivant :
<td align=»right» colspan=»3»>
<span class=»error»>Veuillez sélectionner
un magasin.</span><br><br>
</td>
</tr>
<tr>
<td><input type=»checkbox» name=»accept» checked></td>
Nous pouvons constater que nous avons bien coché la case concernant les conditions générales de vente. En revanche, la sélection du magasin ne s’est pas passée comme prévu.
Nous allons ajouter le code suivant pour détecter et afficher les erreurs, avant de recommencer nos essais :
if ( $m->content() =~ m!(.*?)! ) {
die “Erreur lors de la confirmation : $1”;
}
Si on regarde plus attentivement le formulaire, on voit que le champ loc_id est en fait présent de deux façons dans le formulaire : comme un champ texte caché et comme une liste. WWW::Mechanize fournit la méthode select() qui simplifie la gestion des champs de type select/option. Nous allons donc essayer de sélectionner le magasin avec :
$m->select( loc_id => $magasin );
Cette fois, c’est WWW::Mechanize qui affiche un avertissement Input «loc_id» is not type «select». Et notre script affiche l’erreur Erreur lors de la confirmation : Veuillez sélectionner un magasin..
Le problème est le suivant : nous avons deux champs qui portent le même nom. HTML::Form nous laisse remplir le premier champ loc_id (de type hidden) et envoie alors comme corps de la requête search_zip=&search_city=&loc_id=29729&loc_id=.
Le serveur à l’autre bout doit probablement écraser la première valeur (29729) avec la seconde (vide). Lors de notre deuxième tentative (avec select()), le problème est cette fois que le premier champ nommé loc_id trouvé par WWW::Mechanize n’est pas de type select mais de type readonly.
Pour envoyer une requête correcte au serveur, nous devons donc mettre à jour le second champ loc_id. Nous allons d’abord devoir le sélectionner avec la méthode find_input() de HTML::Form, puis le remplir avec la méthode value() (c’est un objet de type HTML::Form::Input) :
$m->current_form()->find_input( ‘loc_id’, ‘option’ )->value( $CONF{shop} );
Cette fois-ci tout se passe (enfin !) comme prévu, et nous recevons non pas un 302 (que WWW::Mechanize aurait suivi tout seul) mais la page suivante (remise en forme pour des raisons de lisibilité) :
<html>
<head>
<meta http-equiv=»refresh»
content=»0;http://asp07.photoprintit.de/microsite/summary.php» />
<script type=»text/javascript»>window.location.replace
(«http://asp07.photoprintit.de/microsite/summary.php»)</script>
</head>
<body>
<a href=»http://asp07.photoprintit.de/microsite/summary.php»>Klicken Sie hier,
falls Ihr Browser Sie nicht automatisch weiterleitet</a>
</body></html>
Babelfish traduit grossièrement le texte en allemand par « cliquetez ici, si votre Browser ne vous transmet pas automatiquement », ce que nous traduirons en Perl par :
$m->follow_link( url_regex => qr/summary/ );
Une fois le lien suivi, une page résumant la transaction est affichée. Les champs utiles à récupérer sont le numéro de commande et le numéro de client (vous connaissez normalement l’adresse du magasin !), que l’on peut récupérer tous les deux avec une seule expression régulière (à condition d’être bien sûr de l’ordre dans lequel ils apparaissent dans la page).
Le HTML en question ressemble à ceci (la page envoyée est en UTF-8) :
<p>
N° de commande: 235484</p>
<p>
<table border=»0» cellpadding=»4» cellspacing=»0»>
<tr>
<td>
<h2>La livraison s’effectuera au magasin :</h2>
</td>
</tr>
<tr>
<td>
<b>PHOTO STATION (Numéro de client: 813023)<br>
47 Rue Victor Hugo<br>69002 Lyon<br></b>
</td>
Et l’expression régulière qui nous capture toutes les informations d’un coup est la suivante (en évitant soigneusement les caractères non-ASCII) :
my ( $commande, $client ) =
$m->content =~ /(?:commande:\ |client: )(\d+)/g;
Vous recevrez de toute façon un email de confirmation avec le numéro de commande et l’adresse du magasin (mais pas le numéro de client) dans votre boîte aux lettres.
Script complet
Le script suivant est fourni avec de multiples options de ligne de commande qui n’ont pas été décrites dans l’article.
Notez en particulier l’option --list qui se contentera d’envoyer une image PNG de 89 octets (soyons gentils avec les disques durs du fournisseur) et d’aller jusqu’au formulaire de sélection du magasin pour nous afficher la liste complète. Sauvez cette liste dans un coin de votre ~ ou au moins le numéro du magasin le plus près de chez vous. Cette technique d’envoi direct d’une image montre qu’on n’a pas besoin de passer par des fichiers sur le disque pour envoyer des « fichiers » à un serveur web.
L’option --find accepte une expression régulière pour trouver une sous-liste de magasins correspondants dans la liste ainsi obtenue.
Note :
Dans le code, on utilise qr/(?=)/ comme valeur pour $re afin d’avoir une expression régulière qui correspond avec n’importe quelle chaîne (qr/.*/ fonctionnerait tout aussi bien). L’utilisation de qr// pose en effet problème car il s’agit une écriture particulière (tout comme //) : quand l’expression régulière est une chaîne vide, Perl utilise la dernière expression régulière qui a établi une correspondance avec succès.
Si ni --list ni --find ne sont pas fournies, le script insistera pour voir l’option --shop suivie du numéro d’un magasin. Et ira jusqu’au bout de la commande.
Le hash %CONF est fait pour que vous y mettiez vos valeurs par défaut (en particulier pour email, password et shop).
<p>
N° de commande: 235484</p>
<p>
<table border=»0» cellpadding=»4» cellspacing=»0»>
<tr>
<td>
<h2>La livraison s’effectuera au magasin :</h2>
</td>
</tr>
<tr>
<td>
<b>PHOTO STATION (Numéro de client: 813023)<br>
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use WWW::Mechanize;
use IO::Handle;
# pas de tampon sur STDOUT
STDOUT->autoflush(1);
# options par défaut
my %CONF = (
email => ‘’, # À remplacer par vos identifiant,
password => ‘’, # mot de passe
shop => ‘’, # et magasin préféré
find => ‘’,
list => 0,
);
# message d’information
my $USAGE = << ‘USAGE’;
Options disponibles:
--email <email>
--password <password>
--shop <numéro magasin>
--find <regexp>
--list
USAGE
GetOptions( \%CONF, “email=s”, “password=s”, “list!”, “shop=i”, “find=s”,
“help!” )
or die $USAGE;
# divers cas d’erreur
$CONF{$_} or die “$_ requis\n$USAGE” for qw( email password );
die “Au moins un paramètre --shop ou --find ou --list requis\n$USAGE»
unless $CONF{shop} || $CONF{find} || $CONF{list};
die «Au moins une image requise\n$USAGE» if $CONF{shop} && @ARGV == 0;
die $USAGE if $CONF{help};
# connexion à la page principale
my $m = WWW::Mechanize->new;
$m->get(‘http://www.photostation.fr/index.php’);
# client HTML
$m->follow_link( url_regex => qr/htmlclient/ );
# formulaire d’identification
$m->set_fields(
external_login_user => $CONF{email},
external_login_pwd => $CONF{password},
);
$m->click(‘external_login[]’);
die “Échec de l’identification\n» if $m->content !~ /\Q$CONF{email}\E/;
# les noms des photos sont dans @ARGV
if ( $CONF{shop} ) {
if ( @ARGV > 10 ) {
$m->form_number(2);
bless $m->current_form()->find_input(‘quantity’),
‘HTML::Form::TextInput’;
$m->field( quantity => scalar @ARGV );
$m->click;
}
$m->form_number(3);
$m->set_fields( «file$_» => $ARGV[$_] ) for 0 .. @ARGV - 1;
my $req;
$|++;
{
no warnings;
local $HTTP::Request::Common::DYNAMIC_FILE_UPLOAD = 1;
$req = $m->current_form()->make_request;
my $content_sub = $req->content();
my $done = 0;
my $total = $req->content_length;
$req->content(
sub {
my $data = $content_sub->();
return unless defined $data;
$done += length $data;
printf «\rTransfert: %2d%% (%d/%d)», $done / $total * 100,
$done, $total;
return $data;
}
);
}
# envoi des photos
$m->request($req);
print «\n»;
}
else {
# envoi d’une image bidon (blanc 10x10)
$m->form_number(3);
my $input = $m->current_form()->find_input(‘file0’);
$input->content(
pack( ‘H*’,
‘89504e470d0a1a0a0000000d494844520000000a0000000a0103000000b7’
. ‘fc5dfe00000006504c5445000000ffffffa5d99fdd0000000e4944415478’
. ‘da63f87f8001370200075211774b8b03d80000000049454e44ae426082’ )
);
$input->filename(‘blank10x10.png’);
$input->headers( content_type => ‚image/png‘ );
$m->click;
}
die “Échec de l’envoi des images : « . $m->res()->status_line()
unless $m->success();
# taille par défaut
# force le format 10x13
$m->form_number(2);
$m->set_fields(
‘orderdata[4]’ => 0, # 9x11
‘orderdata[5]’ => 1, # 10x13
‘orderdata[11]’ => 0, # 11x15
‘orderdata[6]’ => 0, # 13x17
);
$m->click(‘order[]’);
die “Échec de validation du format : « . $m->res()->status_line()
unless $m->success();
# choix de la livraison en magasin
$m->form_number(2);
$m->field( transfer_type => ‘FC’ );
$m->click(‘startorder[]’);
die „Échec de validation de la livraison : « . $m->res()->status_line()
unless $m->success();
# liste des magasins
$m->form_number(2);
if ( $CONF{shop} ) {
# validation finale
$m->current_form()->find_input( ‘loc_id’, ‘option’ )->value( $CONF{shop} );
$m->tick( ‘accept’, ‘on’ ); # accepte les conditions générales de vente
$m->click(‘startorder[]’); # validation
die “Échec de la confirmation finale : « . $m->res()->status_line()
unless $m->success();
# détection d’éventuelles erreurs
if( $m->content() =~ m!(.*?)! ) {
die «Erreur lors de la confirmation : $1»;
}
# suit la redirection
$m->follow_link( url_regex => qr/summary/ );
die “Échec de récupération du bilan : « . $m->res()->status_line()
unless $m->success();
# affichage des informations de commande
print $m->content();
my ( $commande, $client ) =
$m->content() =~ /(?:commande:\ |client: )(\d+)/g;
print «Numéro de client: $client\n», «Numéro de commande: $commande\n»;
}
else {
my %cities;
my $input = $m->current_form()->find_input( ‘loc_id’, ‘option’ );
@cities{ $input->possible_values() } = $input->value_names();
# recherche et affichage des éléments qui correspondent
my $re = $CONF{find} ? qr/$CONF{find}/i : qr/(?=)/;
print map { “$_\t$cities{$_}\n” }
grep { $cities{$_} =~ $re }
keys %cities;
}
Considération sur la généralisation de ce script
Nous avons vu que le site web d’envoi des photos est en réalité géré par photoprintit.de. Des URL que nous avons manipulés, il n’est pas difficile de déduire que l’identifiant de Photo Station est le 1156.
L’uniligne suivant va nous permettre de découvrir la liste des identifiants valides pour les autres clients de photoprintit.de :
$ perl -MLWP::Simple -le \
‘(get(«http://asp.photoprintit.de/microsite/$_/htmlclient.php»)
ne «Unknown customer id» ) && print «ok $_» for 1 .. 8000’
ok 1
ok 5
ok 8
ok 10
ok 13
...
Ce script s’appuie sur le fait que la page renvoie Unknown customer id si on essaye de passer un identifiant invalide dans l’URL.
Cette recherche sur les 8000 premiers identifiants en a renvoyé 434 valides. Autrement dit, si nous arrivons à faire un module générique, il pourra servir pour chacun des 434 magasins de photo en ligne (la majeure partie d’entre eux sont des sites en allemand ou en hollandais).
Le code que nous avons produit est déjà très générique : nulle part, nous n’avons utilisé autre chose que les noms des champs ou les URL pour nous déplacer de page en page.
Remerciements :
Merci à David Morel (de Lyon.pm) de m’avoir fourni son script de connexion à eBay, à partir duquel j’ai développé ma propre version décrite dans cet article.
Merci à Michel Grafmeyer et aux mongueurs de Perl pour leur travail de relecture.
Comme pour l’article précédent, les deux scripts (eBay et PhotoStation) sont fournis sur le CD. Ils sont également en ligne aux adresses http://articles.mongueurs.net/magazines/webrobot-02/ebay-zero.pl et http://articles.mongueurs.net/magazines/webrobot-02/photostation.pl.


