{"word_count": 3184, "character_count": 20782}
Âges déclarés et sites de rencontres : une procédure de collecte automatisée de données sur internet Ce billet fait suite au tour d’horizon des techniques de collecte automatisée de données sur le web, publié récemment sur ce site. Il offre un exemple commenté de collecte automatique de données via R. D'autres sont disponibles sur le carnet Data Sciences Sociales.S'il est un fait sur lequel s'accordent toutes les études sur les sites de rencontre, c'est la tendance de leurs utilisateurs à sous-déclarer leur âge. Il est souvent démontré en comparant les profils en ligne à une source d’information plus fiable, le plus souvent un entretien. On propose de retrouver ce résultat  à partir d’une analyse quantitative faite à distance, sans aucune rencontre avec un usager, et en quelques minutes. Disons-le immédiatement, les résultats obtenus sont moins riches et plus incertains qu'une véritable enquête. Mais parce que l’objectif est d'illustrer les techniques de collecte automatique de données, on repart de ce fait bien établi pour proposer une méthode pas-à-pas. Apparaîtra alors un apport majeur de ces procédures : l'élargissement immédiat du questionnement sociologique.[caption id="attachment_726" align="aligncenter" width="482"] Distribution de l’âge déclaré des annonceurs sur Craigslist New York (par sexe, et selon le sexe de la personne recherchée)[/caption]Pour ce faire, on part de Craigslist, un site de petites annonces généralistes. Gratuit, il est aussi facile d’accès puisque chacun peut poster des annonces en quelques minutes. Créé au milieu des années 1990, le site s’est depuis beaucoup développé : au début des années 2010, il est présent dans plus de 70 pays, dispose de sites spécifiques pour 700 villes. Il est aussi parmi les plus visités au monde ((Dans les 50 premiers mondiaux, et dans les 10 premiers pour les États-Unis (source : Alexa.com, 22 août 2012).)).Parmi une grande variété de types d’annonces, Craigslist dispose d’une catégorie « Rencontres » ; elle est elle-même séparée en sous-catégories : « femmes cherchant des hommes » (women for men, w4m ci-après), « hommes cherchant des femmes » (men for women, m4w), « femmes cherchant des femmes » (w4w), « hommes cherchant des hommes » (m4m), ainsi que d'autres plus exotiques.[caption id="attachment_728" align="aligncenter" width="446"] Une page de Craigslist (rencontres pour femmes hétérosexuelles)[/caption]Les annonces sont présentées de manières linéaires, par groupe de 100, avec à chaque fois quelques informations sommaires : un bref titre, le lieu de résidence et l’âge de la personne qui passe l’annonce. Si on clique sur l’un de ces liens, on arrive sur une autre page qui donne accès à l’annonce intégrale, et le cas échéant à certaines photos (attention, c’est parfois NSFW – not suitable for work – comme on dit sur les sites anglo-saxons). Le site garde ensuite une archive de ces annonces pendant plusieurs jours.C’est sur ce site, à la fois rudimentaire et très fréquenté, qu’on va tenter de collecter une information : l’âge (déclaré) des personnes qui postent des annonces sur ce site. Afin de rendre les choses claires, on procède en deux temps. On explique d’abord comment récupérer ces informations sur une seule page, puis on présente la technique qui permet d’automatiser ces requêtes.Pour ce faire, on utilise le logiciel statistique open source R (R Development Core Team, 2012). S’il est un au premier abord difficile d'accès pour les non-spécialistes, son caractère gratuit et sa communauté d’aide en ligne en font un outil très complet qui mérite largement l’investissement de départ. Il fait, de plus en plus, partie de la panoplie ordinaire des chercheurs en sciences sociales.1. Analyse d’une pageAnalysons une page de la rubrique « Men seeking Women » de la version new-yorkaise de Craigslist. Comme on le constate si on va à l’adresse suivante http://newyork.craigslist.org/m4w/, chaque annonce ou presque contient l’âge de l’annonceur. C’est cette information qu’on va collecter. On va pour cela utiliser trois fonctions communes de R : readLines, grep et gsub ((Il existe de nombreuses introductions à R disponibles sur internet. Voir par exemple le site pédagogique (et en français) de Julien Barnier : http://alea.fr.eu.org/pages/intro-R. Voir aussi Cornillon, 2008.)) : readLines sert à télécharger une page web (HTML) donnée ; grep est une fonction de recherche, qui se base sur les expression régulières. Elle sélectionne toutes les lignes qui présentent un pattern (motif) particulier : par exemple, toutes les lignes dans lesquelles on trouve une forme textuelle donnée (comme une lettre, un mot, une question, un nombre, etc.), que l’on décrit avec la syntaxe des expressions régulières (voir plus loin pour une explication rapide de cette syntaxe) ; gsub fonctionne sur le même principe, sauf que l’opération effectuée est là un « rechercher-remplacer ». Elle s’appuie également sur les expressions régulières. On cherche un pattern, et on le remplace par quelque chose d’autre que l’on spécifie.Avec ces fonctions de base, on peut déjà obtenir beaucoup de résultats. Partons directement du code, qu’on commente ligne à ligne. Il peut sembler compliqué, mais c’est une syntaxe compacte pour trois opérations dont le principe est simple : on télécharge une page, puis on en sélectionne certaines lignes, et enfin on effectue une sorte de « rechercher-remplacer » sur ces lignes.    1. page &#60;- readLines("http://newyork.craigslist.org/m4w")    2. ages &#60;- grep("- [0-9]+", page, value=TRUE)    3. ages &#60;- gsub(".*- ([0-9]+)&#60;/a&#62;", "\\1", ages)La première ligne va chercher la page internet, et la lit. Elle stocke dans une variable page (vous pouvez taper page, vous verrez le résultat – pour l’instant assez obscur puisqu’il s’agit du code source de la page, en bloc). Les lignes 2 et 3 cherchent à repérer un motif, et à l’isoler – en l’occurrence les âges des personnes. Là, on a besoin de deux types de connaissances pour en comprendre la signification.Lire le code source HTMLD’abord, il faut accéder au code source de la page internet qu’on cherche à capturer. Cela se fait en cliquant sur la page, puis clic-droit &#62; Afficher le code source.Dans ce cas, on se rend compte qu'à chaque petite annonce correspond à un bloc de texte. Il s’agit du code qui est interprété par le navigateur pour produire et afficher la page web qu’on regarde, ni plus ni moins. Comme on le voit dans la capture d’écran ci-dessous, c’est dans ce code que se trouvent les informations qui nous intéressent.[caption id="attachment_729" align="aligncenter" width="443"] Code source d’une page de Craigslist[/caption]Chaque annonce contient une série d’instructions qui commandent son affichage, ainsi que les informations propres à chaque annonce. C’est de ce maelström de lignes de codes qu’on va essayer de tirer des informations plus précises, en ayant confiance dans le principe suivant : ce qui est affiché par le navigateur n’étant que l’interprétation du code source, si on voit des régularités sur la page que le navigateur affiche, alors ces régularités sont présentes également dans le code source. Si on regarde en détail, on voit déjà que l’âge est présent, et on tente de l'isoler. Il faut pour cela utiliser des expressions régulières.Utiliser des expressions régulièresLes expressions régulières (regular expressions, parfois traduites en français comme « expressions rationnelles », et qu’on abrège souvent en regex) sont des outils qui permettent de décrire un motif dans une chaîne de caractères. Il s’agit d’un langage abstrait (des séries de signes pas forcément évidentes à comprendre au premier abord), mais finalement assez simple d’usage : c’est un langage difficile à lire, mais facile à écrire.Les expressions régulières sont une généralisation des fonctions de recherche et de rechercher-remplacer. Ainsi, dans les cas les plus simples, leur syntaxe est triviale : chercher un mot se fait en cherchant la regex écrite de la même manière. Là où cela devient intéressant, c’est pour chercher des formes textuelles plus abstraites, par exemple, pour chercher « un nombre à plusieurs chiffres ». La syntaxe des expressions régulières est construite selon le principe suivant : c’est une suite de classes de caractères (par exemple : la famille des chiffres, qu’on écrit [0-9] ((Ou bien \\d, en R.))), à chaque fois munie d’un quantificateur (par exemple : un ou plus, qui s’écrit +).On ne va pas rentrer ici dans la description détaillée et exhaustive de cette syntaxe, mais c'est le principe. Pour les curieux, de nombreuses introductions sont disponibles sur internet ((Par exemple, voir http://www.regular-expressions.info/reference.html.)).La fonction de recherche : grepLes deux fonctions suivantes, grep et gsub, utilisent des expressions régulières.À la ligne 2, la fonction grep demande : dans l’objet page qu’on a créé, et qui contient l’ensemble des lignes de la page qu’on souhaite analyser, de conserver uniquement (grep) les lignes qui contiennent un certain motif, et de ranger le resultat dans un nouvel objet appelé ages.Le motif est le suivant : on a remarqué, dans le code source, que l’âge est toujours situé après un tiret (« – ») et un espace. Il prend donc la forme « – XX », où XX est l’âge qu’on cherche à extraire. On va demander à R d’aller chercher toutes les lignes (et uniquement celles-ci) où un tel motif existe. C’est ce que fait la ligne 2 du programme :    ages &#60;- grep("- [0-9]+", page, value=TRUE)Le motif se trouve entre les premiers guillemets. En termes plus civils, la fonction dit la chose suivante :« Quand tu trouves une ligne contenant un tiret suivi d’un espace suivi d’une série de chiffres (ce qui peut s’écrire, en syntaxe regex, « – [0-9]+ »), alors conserve cette ligne (et, sous-entendu, laisse tomber les autres) ».[0-9] désigne une classe de caractères, en l’occurrence tous les chiffres ; et le quantificateur + signifie « un ou plus ». Ainsi, [0-9]+ signifie « un ou plusieurs chiffres ».L’option value=TRUE demande de renvoyer le contenu de la ligne (sinon, la fonction renverrait les numéros des lignes, qui n’ont aucun intérêt pour nous ici).C’est bien ce que l’on obtient : si vous regardez le résultat dans ages, vous avez une liste bien plus petite de lignes que dans pages, et bien plus homogène. En voilà un extrait :    [1] "&#60;a href=\"http://newyork.craigslist.org/mnh/m4w/3203062915.html\"&#62;Intellectual Intercourse and the Conversational Climax - 32&#60;/a&#62;"    [2] "&#60;a href=\"http://newyork.craigslist.org/mnh/m4w/3181548270.html\"&#62;Seeking Asian female, 25-45 - 55&#60;/a&#62;"    [3] "&#60;a href=\"http://newyork.craigslist.org/mnh/m4w/3203042424.html\"&#62;Lets chat and keep one another some company.... - 31&#60;/a&#62;"    [4] "&#60;a href=\"http://newyork.craigslist.org/que/m4w/3203023641.html\"&#62;Ищу жену Russian - 31&#60;/a&#62;"    [5] "&#60;a href=\"http://newyork.craigslist.org/mnh/m4w/3203023249.html\"&#62;It's time once again to play [truncated]On a donc réduit le nombre de lignes à celles qui contiennent les âges, mais on n’a pas encore isolé l’information qui nous intéresse, et seulement celle-là. Il faut maintenant faire du nettoyage ; c’est le but de la ligne 3 de notre code.Rechercher-remplacer : gsub    ages &#60;- gsub(".*- ([0-9]+)&#60;a&#62;", "\\1", ages)Par différence avec grep, la fonction gsub repère un motif, et le remplace par autre chose (en laissant inchangées les lignes où ce motif est absent).Ici, on dit littéralement : « Dans ages, à chaque fois que tu rencontres le motif (premier terme entre guillemets), remplace-le par ce qui correspond à ce qui est mis entre parenthèses dans le motif (\\1) » ((\\1 reporte ce qui correspond au contenu de la première paire de parenthèses, dans le motif recherché ; \\2 serait pour la deuxième paire de parenthèses, si le motif en avait, etc.)).Le motif est ici un peu plus complexe, mais finalement moins que ce dont il a l’air. On connaît déjà [0-9]+, dont on a dit plus haut qu’il voulait dire « au moins un chiffre » ; .*, placé avant, veut dire « une série de caractères quelconques » ((On pourrait aussi utiliser ici .*?, qui signifie « une série de caractères quelconques, la plus courte possible ».)) ; &#60;/a&#62; est une balise présente dans le code source de la page. Elle est donc indiquée telle quelle : elle désigne simplement la suite de caractères &#60;/a&#62;.On a donc le motif suivant : - [0-9]+&#60;/a&#62;, duquel on veut extraire la valeur numérique trouvée par [0-9]+. Pour cela, on le place entre parenthèses, et on le retrouve avec \\1.Donc ce que demande la commande, c’est :« Si tu trouves quelque chose, aussi long que possible (.*), suivi d’un tiret, suivi d’un espace, suivi d’un nombre (i.e. un ou plusieurs chiffres), suivi de &#60;/a&#62;, alors conserve le nombre et ajoute-le à la nouvelle variable ages ».C’est bien ce qu’il fait, on peut le vérifier en explorant l’objet ages obtenu à la fin : on a bien une série de nombres à deux chiffres, qui ressemblent aux âges sur la page internet initiale - plus les lignes où cette forme n'a pas été trouvée, qui restent telles quelles. Si vous n’avez pas tout suivi, un tutoriel sur les regex vous éclairera.On a donc réussi à extraire d’une page web des données qui y étaient contenues, ici concernant les âges des annonceurs.2. Collecter des données sur plusieurs pagesMaintenant qu’on a collecté les résultats pour une page, il faut les collecter sur l’ensemble des pages. Cela nécessite de procéder en deux étapes : Avoir une liste de l’ensemble des pages à visiter ; Appliquer le traitement ci-dessus à l’ensemble de ces pages.Dans notre cas, l’une et l’autre de ces deux étapes sont relativement simples.Créer une liste des pages à visiterCraigslist garde en mémoire les annonces pour quelques jours, donc on a une petite archive. La technique manuelle pour aller fouiller les archives est d’aller cliquer sur le bouton « next 100 postings », en bas de la page. On pourrait écrire un petit programme qui le ferait, mais ce serait inutilement complexe pour cette initiation. Comme beaucoup de sites, ce « comportement » (le clic) a en effet une conséquence directe sur l’adresse de la page (l’URL, pour être pédant). En effet, en passant de la page 1 à la page 2 puis 3 et 4, on passe :de http://newyork.craigslist.org/m4w/,à http://newyork.craigslist.org/m4w/index100.html,puis http://newyork.craigslist.org/m4w/index200.html,et http://newyork.craigslist.org/m4w/index300.html,et ainsi de suite.La première page mise à part (et on verra qu’on peut trouver une astuce), toutes les pages portent le numéro du nombre du premier post de la page : 100 pour la page 2, 200 pour la page 3, 300 pour la page 4, etc. Si on augmente le nombre de 100 pour chaque page, on accède à la page suivante. C’est donc une suite arithmétique simple, qu’on peut demander à R de construire tout seul avec la commande suivante :    urls &#60;- sprintf("http://newyork.craigslist.org/m4w/index%s00.html", 0:64)où %s indique la variable qui prend les valeurs situées à droite, à savoir de 0 à 64 (on a vérifié auparavant le nombre de pages).L’objet urls contient donc 65 adresses construites sur le modèle indiqué entre guillemets, à cette seule différence que le %s est remplacé par une valeur allant de 0 à 64.On a donc la liste suivante, qui représente les adresses des 65 pages qu’on veut analyser (on s’était assuré auparavant qu’il y en avait 65, mais s’il y en avait eu moins, ça aurait tout aussi bien fonctionné).    [1] "http://newyork.craigslist.org/m4m/index000.html"    [2] "http://newyork.craigslist.org/m4m/index100.html"    [3] "http://newyork.craigslist.org/m4m/index200.html"    …Écrire un programme qui automatise la collecteOn va maintenant écrire un programme qui automatise la collecte. Comme il reprend beaucoup des éléments exposés ci-dessus, le plus dur est fait. On donne d’abord le programme complet, avant de le commenter.    1. ages.total &#60;- list()    2. for(i in 1:length(urls)) {    3.   page &#60;- readLines(urls[i])    4.   ages &#60;- grep("- [0-9]", ages, value=TRUE)    5.   ages &#60;- gsub(".*- ([0-9]+)&#60;/a&#62;", "\\1", ages)    6.   ages.total[[i]] &#60;- ages    7. }Les lignes 3 à 5 ont déjà été présentées ci-dessus. Elles analysent une page, et isolent les informations sur l’âge qui y sont disponibles. Le reste du programme constitue une boucle, une syntaxe demandant de refaire la même procédure pour chaque nouvelle page. Plus précisément :La ligne 1 crée une variable vide, de type liste dans laquelle on va stocker les données,La ligne 2 définit une variable temporaire i, qui va prendre toutes les valeurs (entières) allant de 1 à la longueur du vecteur urls (qui vaut ici 65),La ligne 3 lit le i-ème élément de urls, qu’on avait déterminée pour qu’elle prenne les valeurs des adresses des pages à visiter, donc l’adresse de la i-ème page,Les lignes 4 et 5 traitent la page comme vu au début,La ligne 6 stocke dans le résultat de la page i comme i-ème élément de la liste ages.total,La ligne 7 ferme la boucle.Le résultat final se trouve dans la variable ages.total. La suite, qu’on peut voir dans le programme intégral copié ci-dessous, consiste à coller ensemble tous les éléments de cette liste en un vecteur, puis à effectuer des traitements statistiques élémentaires. On calcule ici une fonction de densité (density) et on trace une représentation graphique simple. On pourrait nettoyer plus encore les données, enlever ceux qui déclarent un âge supérieur à 99 ans, mais tout dépend des questions sociologiques qu’on se pose.Le code complet, donc :    urls &#60;- sprintf("http://newyork.craigslist.org/m4w/index%s00.html", 0:64)    ages.total &#60;- list()    for(i in 1:length(urls)) {      page &#60;- readLines( urls[i] )      ages &#60;- grep("- [0-9]+", page, value=TRUE)      ages &#60;- gsub(".*- ([0-9]+)&#60;/a&#62;", "\\1", ages)      ages.total[[i]] &#60;- ages    }    # Convertit la liste ages.total en un grand vecteur    # (introduit quelques NA: les annonces sans âge déclaré)    ages2 &#60;- as.integer( matrix(unlist(ages.total), ncol=1) )    # Enlève les centenaires déclarés…    ages2 &#60;- ages2[ -c(which(ages2 &#62;= 99)) ]    # Fonction de densité (lissage)    ages2 &#60;- density(ages2, na.rm=TRUE)    # Graphique    plot(ages2,         col="darkred",         main="Age distribution on Craigslist Personals",         xlim=c(10, 70),         ylim=c(0,.10),         sub="New York",         cex.sub=.75,         xlab="")    grid()    legend(10, .1, c("M4W"), col=c("darkred"), pch=15, cex=.55)    mtext( paste("", Sys.Date()) , side=3, line=.5, cex=.7 )Le résultat est le suivant : une fonction de densité qui montre un pic à 29 ans, et quelques bosses à différents âges « ronds » de la vie (35, 39, 49, 59). Si la surreprésentation de personnes qui déclarent avoir 29 ans peut éventuellement être attribuée à une volonté de trouver un partenaire avant la date fatidique de la troisième décennie (mais iraient-ils sur ce site où les annonces semblent plutôt destiné à des rencontres sans lendemain ?), la répétition porte à douter de cette hypothèse. Il semble bien plus plausible de considérer que les gens ont menti sur leur âge, et qu’ils l’ont plus ou moins tronqué autour de ces frontières.On peut reproduire cette analyse aux autres catégories, ou l’étendre à d’autres villes. À quelques variations près, l’idée initiale se vérifie : on trouve un pic autour de 29 ans, puis selon les groupes ou les villes, aux années en -5 ou en -9 de chaque décennie. La probabilité statistique que les personnes de cet âge là exactement se connectent en surnombre étant ridiculement faible, on peut considérer que le phénomène est avéré.De plus, et c'est là un apport central de la collecte automatisée de données, on peut obtenir sans effort les distributions des âges des « offreurs » pour les autres catégories présentes sur le site : le coût de réplication de la procédure de collecte est quasiment nul – à l’inverse d’une saisie manuelle. Il en est de même pour les autres villes disposant d’un site Craigslist, car la structure du code est la même. On peut ainsi comparer ces différentes sous-populations, et formuler rapidement des hypothèses sociologiques qui dépassent la question de départ, ici sur les âges de la clientèle de ce type de sites, mais aussi les comparaisons entre villes, ou constater le sex-ratio différentiel.[caption id="attachment_726" align="aligncenter" width="482"] Distribution de l’âge déclaré des annonceurs sur Craigslist New York (par sexe, et selon le sexe de la personne recherchée)[/caption][caption id="attachment_727" align="aligncenter" width="457"] Distribution de l’âge déclaré des annonceurs sur Craigslist (hommes homosexuels, par ville)[/caption]