mots-clefs : programmation, Perl, CGI, Web, HTML, HTTP, LWP, wget, Lynx, client

[petit logo]

Collecte automatique/programmée de données et de renseignements sur les sites Web

[Imprimer…]

Paru dans DREAM n°58 / janvier 1999 - YannickCadin@DIABLOTIN.COM

À force de développer les interfaces graphiques, l’interaction et l’ergonomie, on en vient à oublier, pire à concevoir plus difficilement, la création d’automates qui feraient le travail à notre place de façon régulière et autonome. Il est ahurissant de constater quotidiennement le temps perdu en travaux répétitifs sur un ordinateur censé pourtant nous faire gagner en productivité.

Ne nous y trompons pas, il n’est nullement question ici de programmer des « aspirateurs » de sites tels que wget ou Websnake, ce serait quelque peu réducteur. Ceux-ci sont initialement développés pour rapatrier des hiérarchies entières de documents statiques diffusés par ces sites, principalement en vue d’une consultation locale pour des raisons d’économie (limitation des durées de communication) ou pour la gestion de sites miroirs. Si tel est le cas, je vous invite à vous pencher sur l’utilitaire wget (fourni en standard sur les CD de la Red Hat 5.X) qui remplit parfaitement cette fonction. À titre anecdotique, je vous livre ci-dessous la commande qui permet de télécharger les images recto des pochettes de CD proposés par la FNAC sur leur site. (Réfléchissez à deux fois avant de lancer cette commande car il y a environ 37 000 fichiers, représentant un peu plus de 600 Mo) :

wget -b -o pochettes_CD.log -t 10 -T 600 -r -L -np http://www.fnac-direct.fr/images/disques/recto

Non, je le répète, le problème un tout petit peu plus complexe puisque l’idée est essentiellement de simuler l’envoi d’un formulaire, présenté normalement au format HTML à un utilisateur, comme s’il avait été rempli et renvoyé manuellement par ce dernier.

Un peu plus complexe, mais pas la mer à boire tout de même.
Les différentes étapes consistent en : accès au formulaire à l’aide d’un navigateur quelconque, sauvegarde du source de la page HTML, analyse de son contenu et plus particulièrement la zone délimitée par les balises <FORM></FORM> (attention, plusieurs couples de ces balises peuvent coexister au sein de la même page, ce qui ne présente aucune erreur ni incohérence d’un point de vue logique ou syntaxique), détermination des différentes variables présentes, de leurs valeurs et caractéristiques possibles et même de leur ordre d’apparition dans le document.

L’ordre d’apparition des couples « variable=valeur » dans la requête à rédiger ne devrait pas être pris en compte par les scripts CGI mais vous pouvez être certain que bon nombre de prétendus spécialistes du Web ne respectent pas ce principe.

En guise d’application typique, vous pouvez regarder le source de notre script de conversions de devises qu’il suffit d’invoquer avec pour seul paramètre le montant de Dollars à convertir en Francs.

En fait, il s’agît juste d’un appel à Lynx, navigateur Web en mode texte (employé sur les terminaux privés de capacités graphiques et aussi sur les serveurs Videotex qui assurent un relais vers le Web pour de simples Minitels en mode 80 colonnes) de façon non interactive (implicite avec le paramètre ‘-post_data’) que l’on a ‘encapsulé’ dans un script Perl et dont la sortie est redirigée sur un descripteur de fichier (‘LYNX’) afin d’en analyser le contenu pour y trouver une ligne contenant ‘United States Dollars = X,XXX.XX FRF’.

Le paramètre facultatif ‘-nolist’ évite que Lynx conclut sur un récapitulatif de tous les liens trouvés dans le document reçu.

Pour voir le formulaire HTML correspondant, il vous suffit de consulter la page www.xe.net/currency.

Vous pourrez voir dans le source que la méthode d’envoi des données est de type ‘POST’, si elle avait été de type ‘GET’, le paramètre communiqué à Lynx aurait tout simplement été ‘-get_data’ plutôt que ‘-post_data’ et n’aurait impliqué aucune autre modification. Les trois tirets sont propres à Lynx et lui indique la fin des données à soumettre au serveur. Lynx autorisant l’écriture des valeurs à envoyer sur plusieurs lignes, la présence de ces tirets lui indique leur limite.

Comme vous le constatez, Lynx s’avère excellent pour ce genre d’opération, toutefois, s’il faut le relancer à chaque requête et que, dans la foulée, il faut filtrer le résultat obtenu dans un script, autant gagner du temps et économiser des ressources tout en gagnant la possibilité d’améliorer nos requêtes sur des points particuliers que Lynx ne permet pas de personnaliser. Pour cela, il faut et il suffit d’employer une bibliothèque de fonctions spécialisées, tâche que remplit à merveille LWP pour Perl.

Mise en garde, LWP repose sur un nombre important d’autres bibliothèques, ce qui rend son installation un rien rébarbative. Bibliothèques qui, par ailleurs, servent une grande diversité d’applications, touchant essentiellement aux services Internet (telnet, SMTP, POP, etc.)

Lynx reste pour la suite de nos développements un outil privilégié en raison de sa richesse. En effet, parmi sa multitude d’options, une partie est consacrée à l’analyse du flux de données qui transitent entre le client (le navigateur Web) et le serveur. Ce sont ‘-dump’ et ‘-source’, pour les plus simples, qui affichent le source HTML du document sollicité plutôt que son contenu mis en forme, ‘-trace’ (éventuellement associée avec ‘-tlog’) qui donnent tout le détail des échanges, ‘-mime_header’, option très intéressante qui présente tous les entêtes renvoyés par le serveur et qui précèdent le document.

Revenons une seconde sur l’étape consistant à répertorier les variables contenues dans un formulaire et leurs valeurs possibles (pour les menus et les divers boutons - à cocher, radio ou image -) et éventuellement les contraintes auxquelles elles sont soumises au moins du côté navigateur (option ‘MAXLENGTH’ ou vérifications/modifications en JavaScript). Comme il peut s’avérer fastidieux, et même hasardeux, de tout relever manuellement pour ensuite le remettre en forme dans le cadre d’une requête Lynx par exemple pour ressembler à :

variable_1=valeur_1&variable_2=valeur_2&etc.

Une astuce toute simple consiste à remplacer dans le document HTML sauvé sur votre disque dur (en local donc) l’URL associée à la variable ‘ACTION’ dans la balise ‘FORM’ par quelque chose du genre :

http://localhost/cgi-bin/test-cgi

de sorte que, en l’ouvrant dans votre navigateur et en remplissant les différents champs puis en le soumettant enfin, le script test-cgi vous retournera une valeur pour QUERY_STRING (si la méthode est de type ‘GET’) correctement formulée que vous pourrez utiliser telle quelle dans vos programmes.

Traitons un cas concret, la recherche des coordonnées d’une société localisée quelque part en France.

Facile me direz-vous ! Pour cela il suffit d’aller sur www.pageszoom.com, www.annuaire.laposte.fr ou bien encore www.annu.com. Hum! Hum! Exact sauf que… dans chacun de ces annuaires (et mis à part que le premier est vraiment en dessous de tout tellement il fait l’impasse sur des demandes simples - règlement de compte personnel - ), vous êtes obligé de saisir au moins une référence géographique, ville ou département. Quel plaisir de saisir tour à tour 37000 noms de communes ou alors, dans le meilleur des cas, près d’une centaine de numéros de départements si l’on a aucune idée de la situation géographique de l’entité recherchée.

Par contre, il suffit d’un ‘petit’ script pour résoudre notre problème.

Notez que si ce script, pour des besoins didactiques, utilise LWP, il aurait pu tout aussi bien, et probablement plus facilement, faire uniquement appel à Lynx.

Par contre ce dernier se révélera probablement insuffisant si vous voulez développez un utilitaire équivalent pour chercher l’information sur les sites de la Poste ou de France Telecom.

Et d’ailleurs, je vous invite à plancher sur ce problème car il représente un beau défi (je suis convaincu que c’est réalisable).

Lire la documentation :

pour en savoir plus sur ces trois bibliothèques de fonctions et toutes les options qu’elles offrent. HTTP::Request a notamment l’énorme avantage sur Lynx d’autoriser la définition d’entêtes spécifiques comme par exemple ‘Referer’ qui est parfois employé par les serveurs pour ‘tenter’ de contrôler la provenance de la requête.

Typiquement, l’exploitation d’un tel méta-annuaire passera par un Intranet qui fera office d’intermédiaire entre les utilisateurs et les différents annuaires cités plus haut en présentant son propre formulaire qui invoquera un script CGI dérivé de l’embryon proposé ici.

Penchons-nous dessus quelques instants.

La première opération consiste à remplacer les espaces par le caractère ‘+’ dans le nom passé en paramètre.

La création de l’objet ‘$requete’ stipule que la méthode pour le passage des données au script CGI est de type ‘POST’ (par opposition à ‘GET’) et que l’URL du script est 'http://www.annu.com/annu/cgi-bin/www'.

Dans la boucle qui passera en revue les 95 départements, nous donnons nous-mêmes la valeur correcte pour le champ 'Content-length' qui appartient à l’entête de la requête envoyée au script CGI. Les différents champs obligatoires dans l’entête seront automatiquement renseignés et insérés pour vous par la méthode ‘request’ si vous ne les avez pas défini explicitement avant son appel.

La variable d’instance ‘content’ (dans l’expression ‘$reponse->content’) contient le résultat délivré par le script CGI via son serveur Web dans sa représentation ‘source’. C’est la raison de la présence des deux substitutions dans la procédure ‘traite_reponse’, la première sert à faciliter la délimitation de chaque bloc de lignes constituant les coordonnées d’une personne ou société trouvée, et la substitution suivante élimine toutes les balises HTML pour ne conserver que les informations brutes. Ensuite, chaque bloc commençant par son numéro d’ordre, seul sur une ligne, et finissant par une séquence de quatre tirets, eux aussi isolés sur une ligne, l’extraction des coordonnées s’en trouve simplifiée. Il suffit d’y supprimer les lignes vides et de remplacer le ‘é’ en ‘e’ (cette dernière opération étant inutile sur les systèmes affichant correctement les caractères codés à la norme ISO 8859-1).

Notez que, au besoin, les librairies LWP et associées contiennent toutes les méthodes requises pour connaître tout le détail des échanges entre client et serveur.

Quatre optimisations possibles consistent, d’une part à lancer les requêtes pour tous les départements en parallèle plutôt qu’en séquence ; d’autre part, en présentant les résultats à l’utilisateur au fur et à mesure de leur réception. Ensuite proposer une recherche Région Parisienne ou Province. Enfin, une amélioration d’ordre esthétique consistera à détecter les coordonnées partageant la même adresse pour les regrouper en une seule entité avec la liste des différents services et/ou numéros de téléphones et/ou de télécopieurs.

Attention, le nom à rechercher doit répondre à la syntaxe attendue par un script CGI quant à la représentation des caractères qui, mis à part les lettres, les chiffres et quelques symboles de ponctuation, sont piochés dans la norme ISO 8859-1 (adoptée avec bonheur et discernement par Linux, et accessoirement par pure chance et hasard sur les dernières versions de Winchose) et codés en hexadécimal sous la forme %HH.

À l’inverse, le seul caractère non ASCII présent dans les réponses délivrées par www.annu.com semble être le ‘é’ de ‘Télécopie’ et ‘Numéris’ que nous remplaçons par un ‘e’ à l’aide de ‘s/\351/e/g’.

Soyons clairs, bien que très intéressant, un automate de ce genre qui semble de prime abord simple à programmer révèle tout de même plusieurs cas de figure à prendre en considération comme, exemples très communs, le découpage de la réponse en plusieurs pages HTML contenant un bouton suite qui oblige à soumettre une autre requête au serveur plus ou moins différente de celle de base (raison pour laquelle le source présenté ici se cantonne à n’afficher au plus que les dix premières coordonnées trouvées dans chaque département), et plus souvent encore l’absence de réponse. Autrement dit, il est indispensable de tester manuellement une majorité de cas, de sauver les pages HTML correspondant aux résultats afin de pouvoir les analyser en détail avant de se lancer dans l’écriture d’un client Web automatique.

Un cas particulier reste celui de l’accès à des sites sécurisés avec le protocole SSL. Normalement, nous ne sommes pas censés en parler puisque, dans le cas de Lynx par exemple, les modifications au code développées pour utiliser ce protocole ou même le code exécutable correspondant ne devraient pas être accessibles depuis la France. Et le site officiel de Lynx est assez intransigeant sur ce sujet, qui ne propose un envoi de ceux-ci que par la poste, uniquement sur le territoire américain. Malgré tout, il peut arriver qu’un quidam, moins à cheval sur les lois ou moins averti, dépose sur un site FTP public tout ou partie du code en question, disponible alors sous la désignation lynx-ssl-2.8-1.i386.rpm (mais bon, je ne vous ai rien dit, hein ?)

Notez que vous ne pourrez installer cette archive qu’en ayant préalablement récupéré et installé la librairie SSL (archive SSLeay-0.8.1-4.glibc.i386.rpm par exemple mais… chut !)

Notez que l’on retrouve sur les serveurs très sollicités par le genre d’automates présentés ici des parades similaires à celles apparues avec l’essor du Minitel, la plus simple étant de changer la présentation des données communiquées à l’utilisateur afin de rendre caduque, au moins temporairement, l’analyse contenue dans le script.

Si vous désirez approfondir ce sujet, sachez qu’il existe un ouvrage consacré à ce domaine :

« Programmation de clients Web avec Perl » par Clinton Wong chez O’REILLY.






#!/usr/bin/perl

#	change_devise.pl
#
#		Usage : change_devise.pl montant_en_dollars

$somme = $ARGV[0];
$devise_depart = 'USD+United+States+Dollars';
$devise_arrivee = 'FRF+France+Francs';

open (LYNX, "/usr/bin/lynx -post_data -nolist http://www.xe.net/cgi-bin/ucc/convert<<FIN_REQUETE\ntimezone=Canada%2FEastern&Amount=$somme&From=$devise_depart&To=$devise_arrivee\n---\nFIN_REQUETE\n|");

while (<LYNX>)
{
    if (/United States Dollars[^\d]+([\d,.]+) FRF/)
    {
        ($change = $1) =~ tr/,./ ,/;
        print "Cela fait $change Francs Français\n";
        last;
    }
}
close (LYNX);


#!/usr/local/bin/perl

#	recherche_en_france.pl
#
#		Usage : recherche_en_france.pl organisation

use LWP::UserAgent;
use HTTP::Request;
use HTTP::Response;

($nom_cherche = $ARGV[0]) =~ s/ /+/g;
$formulaire = "country=france&type=0&cv=0&num=0&nom=$nom_cherche&ville=&dep=&rub=";
$agent = new LWP::UserAgent;
$requete = new HTTP::Request ('POST', 'http://www.annu.com/annu/cgi-bin/www');

for ($dept = 1; $dept <= 95; $dept++)
{
    $formulaire =~ s/dep=[^&]*&/dep=$dept&/;
    $requete->header ('Content-length' => length ($formulaire));
    $requete->content ($formulaire);
    $reponse = $agent->request ($requete);
 
    if ($reponse->is_success)
    {
        $total_reponses += traite_reponse ($reponse->content);
    }
    else
    {
        print STDERR "Echec de la rêquete pour le département $dept\n";
        $debug && print $reponse->error_as_HTML;
    }
}
$total_reponses && print "\n$total_reponses réponse(s) trouvée(s)\n";

sub traite_reponse
{
    my ($reponse) = @_;
    my ($trouves, $element, $coordonnees);
 
    if ($reponse =~ /Nombre de personnes trouv.es &nbsp;: (\d+)/)
    {
        $trouves = ($1 > 10) ? 10 : $1;
 
        $reponse =~ s/<HR>/----/g;
        $reponse =~ s/<[^>]*>//g;
 
        for ($element = 1; $element <= $trouves; $element++)
        {
            if ($reponse =~ /\n$element\n(.*?)\n----\n/s)
            {
                $coordonnees = $1;
                $coordonnees =~ s/\n{2,}/\n/g;
                $coordonnees =~ s/\351/e/g;
                print "$coordonnees";
            }
            else
            {
                die "Problème d'analyse de la réponse\n";
            }
        }
    }
    else
    {
        $trouves = 0;
    }
    return $trouves;
}