lundi 3 décembre 2007

Simuler un explorateur web avec C# et .Net

Tous les sites ne proposent pas une API pour récupérer leurs contenus. Toutefois, on veut pouvoir récupérer des informations venant d'eux. Pour ce faire, on a besoin de faire des requêtes HTTP et de parser le résultat pour en tirer ce que l'on veut. Notez qu'on ne fera pas d'interprétation JavaScript dans cet article, cela dépasserait largement le cadre de .Net et requerrait des librairies externes.

Dans cet article, on s'intéressera à la première partie.



Pourquoi est-ce que c'est plus compliqué qu'il n'y paraît ?



Certains sites peuvent avoir intérêt à modifier les résultats selon qui effectue les requêtes. En effet, on peut imaginer qu'une page Web veuille se présenter de façon optimisée pour le référencement lorsqu'un robot passe sur elle, mais présente une version plus jolie et adaptée à un être humain lorsqu'en effet c'est un être humain (ou du moins un client HTTP de type IE, Safari ou Firefox) qui la visite. On peut aussi imaginer d'autres motifs plus fourbes : par exemple, ne pas transmettre les bons résultats afin d'empêcher l'exploitation de données assistée par ordinateur.



Comment va-t-on faire ?



La première chose à faire est de définir l'User-Agent à quelque chose de réaliste. Vous pouvez regarder le vôtre, par exemple, en vérifier cette page (qui donne entre autres l'information nécessaire dans le champ httpuseragent).



Faire des requêtes HTTP avec .Net



.Net propose des classes spécialisées dans les requêtes web. On peut donc travailler avec sans trop se poser de soucis. La classe qui nous intéresse est la classe HttpWebRequest, qui contient vraiment tout ce qu'on peut vouloir.
Pour l'utiliser, on fait ainsi :


HttpWebRequest maRequete = (HttpWebRequest) HttpWebRequest.Create("http://mon-url.com");

Nous devons effectuer un transtypage car la méthode statique Create renvoie un objet WebRequest, plus restreint et moins intéressant.

Pour lancer la requête et récupérer son résultat, on se sert d'un autre objet, HttpWebResponse. Notez qu'ici .Net a un mécanisme de jeté d'exception en cas de problème qui se passerait (problème de DNS, requête expirée...). On doit donc encadrer le déclenchement de la requête dans une structure try. En général en cas de problème, l'erreur renvoyée sera une WebException.
En reprenant le code ci-dessus, on obtient donc

HttpWebResponse maReponse;
try
{
maReponse = (HttpWebResponse) maRequete.GetResponse();
}
catch (WebException) { MessageBox.Show("WebException"); }
catch (Exception e) { MessageBox.Show("Exception non prévue " + e.Message);}


Nous avons maintenant normalement un objet maReponse valant normalement quelque chose de non-nul. Cet objet contient entre autres un Stream qui représente le flux de données envoyé par le serveur HTTP. On le lit comme tout stream. Ici on suppose qu'on va vouloir le lire d'un coup.


StreamReader lecteur = new StreamReader(maReponse.GetResponseStream());
MessageBox.Show(lecteur.ReadToEnd());


Avec ce code, on affiche le contenu de la page qu'on a demandée.



Et cela suffit ?



Malheureusement non. Vous connaissez les cookies ? Ce sont de délicieuses délices (excellente recette disponible ici que je vous recommande) mais surtout des petits morceaux de données que les serveurs web envoient au client pour stocker des informations. Or, par défaut, si on fait nos requêtes successivement, on ne stocke pas les cookies et on recommence de 0 à chaque fois. Les applications web peuvent détecter ce comportement comme caractéristique d'un robot et nous envoyer leurs données "retravaillées", celles que l'on cherche justement à éviter. On doit donc enregistrer les cookies. Et c'est là que .Net est bien fait.



Le principe


Les cookies sont contenus dans une boiteEnFerBlanc (ou une cookieJar, ou conteneur pour les moins inspirés) que l'on assigne à notre requête à chaque fois. On y stocke aussi les cookies que l'application nous envoie. .Net propose pour stocker les cookies un objet CookieCollection, qui n'est qu'une List<Cookie> un peu déguisée et plus archaïque. Toutefois, on ne peut pas juste assigner cette CookieCollection directement à l'objet maRequete. En effet, un cookie ce n'est pas qu'un nom et une valeur. C'est aussi un domaine. Donc, un exemple


CookieCollection boiteEnFerBlanc = new CookieCollection();
boiteEnFerBlanc.Add(new Cookie("Delice1","Au chocolat","/",".mon-domaine.com"));
boiteEnFerBlanc.Add(new Cookie("Delice2","A la vanille","/","web.mon-domaine.com"));
// on doit créer un CookieContainer pour contenir les cookies
maRequete.CookieContainer = new CookieContainer();
// maintenant, on ajoute les cookies qu'on a fabriqués
maRequete.CookieContainer.Add(boiteEnFerBlanc);


Quand on fera le GetResponse(), maRequete enverra les cookies demandés. On doit aussi récupérer les cookies que le site distant va nous envoyer. Pour ce faire, on va réutiliser la boîte en fer blanc.


try
{
maReponse = (HttpWebResponse) maRequete.GetResponse();
}
catch (WebException) { MessageBox.Show("WebException"); }
catch (Exception e) { MessageBox.Show("Exception non prévue " + e.Message);}

foreach (Cookie c in maReponse.Cookies)
{
boiteEnFerBlanc.Add(c);
}


C'est une approche simpliste mais fonctionnelle. Ensuite, à la prochaine création d'une requête, il suffira de réassigner le champ CookieContainer à boiteEnFerBlanc pour restaurer le contexte. On a maintenant un browser réaliste.



Une petite classe browser


Parce qu'un post sur ce blog sans la classe qui va avec ce serait dommage, voilà ce qu'on peut envisager de faire comme classe utilitaire. Ca reste de l'ordre du stub, mais ça doit fonctionner ! (reformaté avec Resharper)


using System;
using System.IO;
using System.Net;

namespace Utilities
{
public class WebBrowser
{
private CookieCollection cookies;
private string userAgent;

public WebBrowser()
{
userAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11";
Reset();
}

public WebBrowser(String userAgent)
{
this.userAgent = userAgent;
Reset();
}

public string UserAgent
{
get { return userAgent; }
set { userAgent = value; }
}

public void Reset()
{
cookies = new CookieCollection();
}

public string Request(String uri)
{
HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;
if (request == null) throw new Exception("Could not create request");
request.CookieContainer.Add(cookies);
request.UserAgent = userAgent;
HttpWebResponse response;
try
{
response = request.GetResponse() as HttpWebResponse;
}
catch (WebException e)
{
return "[ERROR]WebException " + e.Message;
}
catch (Exception e)
{
return "[ERROR]General exception " + e.Message;
}
if (response == null) throw new Exception("Could not get response");
foreach (Cookie c in response.Cookies)
{
cookies.Add(c);
}
return new StreamReader(response.GetResponseStream()).ReadToEnd();
}
}
}


N'hésitez pas à donner votre feedback.



Un dernier mot


Pour avoir une approche réellement réaliste, on doit avoir des requêtes assez espacées, d'au moins 5 secondes. Ce temps peut paraître énorme, mais c'est à vous d'étudier selon la nature du site ce qu'on va adopter comme comportement. Je n'ai jamais entendu parler de site qui fasse du tri là-dessus (et la base de données serait très lourde à gérer...) mais autant se faire les plus discrets possibles, c'est le but de la manoeuvre après tout.

Aucun commentaire: