-
Notifications
You must be signed in to change notification settings - Fork 42
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
--- | ||
title: "Scraping" | ||
--- | ||
|
||
```{r options_communes, include=FALSE} | ||
source("options_communes.R") | ||
``` | ||
|
||
<div class="important"> | ||
Ce chapitre est en cours d'écriture. | ||
</div> | ||
|
||
Une grande partie des données que l'on trouve sur Internet n'y sont pas présentées sous la forme d'un jeu de données : dans de très nombreux cas de figure, ces données peuvent être présentées, par exemple, sous la forme d'un tableau, ou d'une série de pages Web. Ce chapitre explique comment récupérer ces données, de manière à en permettre la manipulation dans **R**. | ||
|
||
La récupération de données numériques, que l'on va illustrer à partir de trois sites Internet consacrés aux théories du complot circulant en France, est plus connue sous le nom de _scraping_ ou de _[Web scraping](https://en.wikipedia.org/wiki/Web_scraping)_. Il s'agit d'un ensemble de techniques, dont on présentera ici que les principaux aspects, appliqués à un cas d'étude précis. | ||
|
||
## Les sources de l'exemple | ||
|
||
Ce chapitre s'intéresse à trois sites Internet consacrés aux théories du complot et à leurs diffuseurs, les « conspirationnistes ». Le site de Rudy Reichstadt, [Conspiracy Watch][d1], qui va devenir notre principale source de données, propose une [définition de ce terme](http://www.conspiracywatch.info/F-A-Q_r11.html). La seconde source utilisée, le site [Confusionnisme][d2] d'Ornella Guyet, utilise une [définition différente](http://confusionnisme.info/vous-avez-dit-confusionnisme/), qui recoupe largement la première du point de vue des individus et des groupes qu'elle identifie. Notre troisième source, le site anonyme [Conspis hors de nos vi[ll]es][d3], ne propose pas de définition précise pour sa part, mais fournit [quelques éléments supplémentaires de description](http://conspishorsdenosvies.noblogs.org/qui-sommes-nous/). | ||
|
||
[d1]: http://www.conspiracywatch.info/ | ||
[d2]: http://confusionnisme.info/ | ||
[d3]: http://conspishorsdenosvies.noblogs.org/ | ||
|
||
Les termes de « théorie du complot » et de « conspirationnisme » étant difficiles à saisir en seulement quelques phrases, on renverra le lecteur à la [note publiée par Rudy Reichstadt](http://www.jean-jaures.org/Publications/Notes/Conspirationnisme-un-etat-des-lieux) pour l'Observatoire des radicalités politiques de la Fondation Jean Jaurès. Cette note donne un bon aperçu des différents groupes impliqués dans la diffusion de ces « théories » en France, que l'on retrouve dans une [cartographie en réseau](http://www.joelgombin.fr/mapping-the-french-conspiracist-web-key-findings/) de leurs sites Internet, réalisée par Joël Gombin en juillet 2014. Les données récupérées dans ce chapitre recoupent les informations fournies dans ces deux sources. | ||
|
||
### Les blogs | ||
|
||
Les sites Internet auxquels on s'intéresse sont tous les trois publiés sous la forme de blogs. Ce détail est important, car pour en récupérer les informations publiées par ces sites, il va falloir comprendre la structure sous-jacente de ces blogs, c'est-à-dire la syntaxe HTML de leurs pages. Les sites [Confusionnisme][d2] et [Conspis hors de nos vi[ll]es][d3] sont les plus simples à comprendre. En effet, ils sont tous les deux publiés grâce au moteur de blog [WordPress](http://wordpress.org/), qui permet de parcourir les différentes pages d'un blog en rajoutant le suffixe `/page/n` à l'adresse-racine du site, de la manière suivante : | ||
|
||
``` | ||
http://confusionnisme.info/page/1 | ||
http://confusionnisme.info/page/2 | ||
http://confusionnisme.info/page/3 | ||
... | ||
http://conspishorsdenosvies.noblogs.org/page/1 | ||
http://conspishorsdenosvies.noblogs.org/page/2 | ||
http://conspishorsdenosvies.noblogs.org/page/3 | ||
... | ||
``` | ||
|
||
En navigant ces liens, on s'aperçoit que les deux sites en question n'ont publié qu'un nombre limité de billets : il n'y a que 4 pages de billets sur le premier, et 5 pages sur le second. Le site [Conspiracy Watch][d1] est, en comparaison, beaucoup plus riche : en effet, comme l'indique le compteur visible en bas de chaque page, le site compte 60 pages de billets, auxquelles le lecteur peut accéder en utilisant un suffixe différent, lié à l'utilisation d'un moteur de blog différent de WordPress. Dans ce cas de figure, le suffixe ne renvoie pas à une « page », mais à un « compteur » de billets, où le dernier billet publié est numéroté `0` : | ||
|
||
``` | ||
http://www.conspiracywatch.info/?start=0 | ||
http://www.conspiracywatch.info/?start=20 | ||
http://www.conspiracywatch.info/?start=40 | ||
... | ||
``` | ||
|
||
Suivant ce schéma de pagination, qui commence à `0` puis augmente de 20 billets par page, la page `60` va correspondre au suffixe `?start=1180`. On connaît donc désormais le nombres de pages à récupérer sur chacun des blogs étudiés, en notant bien que c'est le site [Conspiracy Watch][d1] qui va fournir la très grande majorité des pages. On aurait pu « découvrir » ces informations de manière programmatique, en écrivant un peu de code pour ce faire, mais un repérage manuel du nombre de pages sur chacun des blogs est ici tout aussi rapide, même s'il faudra le mettre à jour lorsque les blogs auront publié de nouvelles pages de billets. | ||
|
||
### Les mots-clés | ||
|
||
Sur chacun des blogs auxquels on s'intéresse, on trouve des billets très détaillés sur tel ou tel groupe diffusant une ou plusieurs « théories du complot ». Sur les blogs [Confusionnisme][d2] et [Conspiracy Watch][d1], on trouve par exemple [deux](http://www.conspiracywatch.info/Mouvement-du-14-Juillet-le-pu-putsch-qui-fait-flop_a1427.html) [articles](http://confusionnisme.info/2015/07/15/le-petard-mouille-du-14-juillet/) sur un groupuscule ayant appelé à un « Mouvement du 14 juillet » 2015. Sur le blog [Conspis hors de nos vi[ll]es][d3], qui a cessé de publier en mars 2012, le [dernier billet](http://conspishorsdenosvies.noblogs.org/post/2012/03/30/les-ideologues-de-la-contre-revolution-sinvitent-a-lageca-et-on-devrait-laisser-faire-ca/) évoque un autre exemple de ces groupes. Ces différents billets sont tous soigneusement catégorisés par de très nombreux mots-clés, qui incluent notamment les noms propres des individus cités ; [ce billet](http://www.conspiracywatch.info/Laurent-Louis-a-propos-du-premier-pas-de-l-homme-sur-la-Lune_a1419.html), par exemple, se termine par les mots-clés suivants : | ||
|
||
``` | ||
11 septembre, antiaméricanisme, apollo 11, etat islamique, etats-unis, laurent louis, lune | ||
``` | ||
|
||
Ces mots-clés sont destinés à permettre aux lecteurs de naviguer plus facilement à travers les différents billets du site, ainsi qu'à faciliter l'indexation du blog par les moteurs de recherche. Ce que l'on se propose de faire ici consiste à récupérer, pour chacun des billets publiés par chacun des trois blogs, l'ensemble de ces mots-clés, ainsi que les titres, les dates de publication et les adresses Internet -- les [URL](https://fr.wikipedia.org/wiki/Uniform_Resource_Locator) -- des billets auxquels ils correspondent. Ces données permettront par la suite de construire un [réseau de co-occcurrences](https://en.wikipedia.org/wiki/Co-occurrence_networks) de ces mots-clés, c'est-à-dire une représentation graphique des associations entre ces mots-clés sur la base des trois sources utilisées. | ||
|
||
## Récupération des données | ||
|
||
Pour récupérer les données des trois blogs, on va commencer par charger quelques extensions utilisées dans plusieurs autres chapitres : l'extension `dplyr`{.pkg} va servir à manipuler les données au fur et à mesure de leur récupération ; l'extension `readr`{.pkg} va servir à sauvegarder le résultat final au format CSV ; l'extension `lubridate`{.pkg} va servir à convertir les dates de publication des billets vers un même format générique ; et l'extension `stringr`{.pkg} va servir à nettoyer le texte récupéré. | ||
|
||
```{r, message = FALSE} | ||
library(dplyr) | ||
library(readr) | ||
library(lubridate) | ||
library(stringr) | ||
``` | ||
|
||
Chargeons à présent l'extension `rvest`{.pkg}, qui va fournir les fonctions essentielles à la récupération des données de chacun des blogs. Comme [l'explique](https://cran.r-project.org/web/packages/rvest/README.html) l'auteur de l'extension, celle-ci est inspirée d'extensions équivalentes disponibles pour le langage Python. Sa fonctionnalité principale est de permettre à l'utilisateur, à l'aide d'une syntaxe simplifiée ou à l'aide de la syntaxe [XPath](https://fr.wikipedia.org/wiki/XPath), de sélectionner les différents éléments d'une page Web, à partir des balises [HTML](https://fr.wikipedia.org/wiki/Hypertext_Markup_Language) et [CSS](https://fr.wikipedia.org/wiki/Feuilles_de_style_en_cascade) de cette page.^[Si vous ne connaissez rien aux langages HTML et CSS, c'est le moment ou jamais d'en apprendre les bases ! Un excellent site de référence pour ce faire est [W3 Schools](http://www.w3schools.com/).] | ||
|
||
```{r, message = FALSE} | ||
library(rvest) | ||
``` | ||
|
||
### Récupération d'éléments HTML | ||
|
||
Commençons par le blog [Confusionnisme][d2]. Un rapide coup d'oeil au [code source de sa page d'accueil](view-source:http://confusionnisme.info/) montre que les billets publiés sur ce blog se trouvent dans une suite de structures : l'une d'entre elles, `<div id="content">`, qui se lit « diviseur à identifiant `content` », contient tous les billets, et à l'intérieur de cette structure, tous les titres de billets se trouvent dans un hyperlien `<a>` à l'intérieur d'une balise `<h1 class="entry-title">`, qui se lit « titre de niveau 1 de classe « entry-title ». | ||
|
||
Récupérons désormais le code source de la page d'accueil du blog grâce à la fonction `html`{data-pkg=rvest}. Une fois exécuté le code ci-dessous, affichez le contenu de l'objet `h` pour réaliser que vous venez de récupérer le code source HTML de la page d'accueil du blog : | ||
|
||
```{r} | ||
h = html("http://confusionnisme.info/") | ||
``` | ||
|
||
Sélectionnons, à présent, toutes les balises correspondant aux identifiants notés ci-dessus, grâce à la fonction `html_nodes`{data-pkg=rvest}. Pour gagner de la place, on n'affichera ici que les deux premiers titres de billets que renvoie cette dernière fonction : | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, "#content .entry-title a") %>% | ||
head(2) | ||
``` | ||
|
||
Le code ci-dessus signifie : « sélectionner tous les hyperliens `<a>`, à l'intérieur des éléments identifiés par la classe `entry-title`, à l'intérieur de l'élément portant l'identifiant `content` ». Comme l'on peut le voir, les identifiants des éléments HTML (`id`), qui sont censés être uniques, sont codés par un dièse (`#`), et les classes de ces mêmes éléments (`class`), qui peuvent se répéter, sont codées par un point (`.`). Ces codes sont identiques à ceux que l'on utilise pour attribuer des styles particuliers à ces éléments en langage CSS. | ||
|
||
Les éléments HTML que l'on a sélectionnés contiennent aussi bien des balises HTML (telles que `<a>` et `<i>`) que du texte. Pour ne sélectionner que le texte, on rajoute la fonction `html_text`{data-pkg=rvest} au code montré ci-dessus. Toujours par économie de place, on ne montre que les deux premiers résultats de ce nouvel enchaînement de fonctions : | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, "#content .entry-title a") %>% | ||
html_text %>% | ||
head(2) | ||
``` | ||
|
||
Voilà qui permet donc de récupérer les titres des billets ! Pour récupérer les hyperliens vers ces billets, rien de plus simple : au lieu de récupérer le texte des titres, il suffit de demander à récupérer l'attribut `href` de chaque lien, en utilisant la fonction `html_attr`{data-pkg=rvest}. On obtient cette fois-ci les hyperliens complets vers chaque billet : | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, "#content .entry-title a") %>% | ||
html_attr("href") %>% | ||
head(2) | ||
``` | ||
|
||
Présentons encore un exemple de sélection d'éléments sur la page d'accueil de ce blog, cette fois-ci en montrant l'intégralité des éléments récupérés, car ils prennent peu de place à l'écran. Ici, on récupère les dates de publications des billets, qui se trouvent, toujours selon le [code source de la page](view-source:http://confusionnisme.info/), dans une balise `<time>` qui se trouve dans une balise `<header class="entry-meta">`. Le code que l'on donne à la fonction `html_nodes`{data-pkg=rvest} est donc : | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, "#content header.entry-header time") %>% | ||
html_text | ||
``` | ||
|
||
On voit bien ici que les deux premières dates sont identiques aux dates qui figurent dans les hyperliens des deux premiers billets, tels que vus plus haut. | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, "#content header.entry-header time") %>% | ||
html_text | ||
``` | ||
|
||
Terminons, enfin, par un exemple plus compliqué. Comme on l'a déjà écrit, chacun des billets du blog est accompagné de plusieurs mots-clés. Après inspection du code source, on voit que ces mots-clés se trouvent regroupés dans un élément appelé `<span class="tag-links">`. Visionnons les deux premiers éléments en question, toujours à l'aide de la même syntaxe de sélection : | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, ".tag-links") %>% | ||
head(2) | ||
``` | ||
|
||
Pour pouvoir stocker tous les mots-clés d'un billet sur la même ligne d'un fichier CSV, qui contiendra aussi le titre du billet, son hyperlien et sa date de publication, il va falloir regroupr ces mots-clés. On va donc, _à l'intérieur de chacun des éléments_ de la liste d'éléments `<span>`, extraire le texte des mots-clés, contenus dans les éléments `<a>`, et les "coller" ensemble grâce à la fonction `paste0`{data-pkg=base} et à son argument `collapse` : | ||
|
||
```{r, tidy = FALSE} | ||
html_nodes(h, ".tag-links") %>% | ||
sapply(function(x) html_nodes(x, "a") %>% | ||
html_text %>% | ||
paste0(collapse = ";")) %>% | ||
head(2) | ||
``` | ||
|
||
L'astuce se trouve ici dans l'utilisation de la fonction `sapply`{data-pkg=base}, qui permet de travailler sur chacun des éléments `<span class="tag-links">` de manière séparée. L'utilisation de la fonction _pipe_ `%>%` a par ailleurs permis de travailler de manière cumulative, par essai-erreur, tout en produisant un code final plutôt lisible. | ||
|
||
### Récupération de plusieurs pages | ||
|
||
On sait désormais comment récupérer les informations que l'on veut collecter. Le blog [Confusionnisme][d2] n'ayant que 4 pages, il va être très simple de les récupérer à l'aide d'une petite boucle qui récupère chaque page, en extrait les données inspectées ci-dessus, et les rajoute à un jeu de données initialement vide, nommé `d1`, grâce à la fonction `rbind`{data-pkg=base} : | ||
|
||
```{r, tidy = FALSE, cache = TRUE} | ||
d1 = data_frame() | ||
for(i in 1:4) { | ||
h = html(paste0("http://confusionnisme.info/page/", i)) | ||
d1 = rbind(d1, data_frame( | ||
url = html_nodes(h, "#content .entry-title a") %>% html_attr("href"), | ||
title = html_nodes(h, "#content .entry-title a") %>% html_text, | ||
date = html_nodes(h, "#content header.entry-header time") %>% html_text, | ||
tags = html_nodes(h, ".tag-links") %>% | ||
sapply(function(x) html_nodes(x, "a") %>% | ||
html_text %>% | ||
paste0(collapse = ";")) | ||
)) | ||
} | ||
``` | ||
|
||
À la date de publication de ce blog, ce petit bout de code récupère les 36 billets étalés sur les 4 pages du site [Confusionnisme][d2]. Comme le montre l'inspection du résultat, le jeu de données que l'on vient de constituer contient l'adresse, le titre, la date de publication et les mots-clés de ces billets : | ||
|
||
```{r, eval = FALSE} | ||
View(d1) | ||
``` | ||
|
||
Il ne reste plus qu'à convertir la variable <var>date</var> vers le format générique `yyyy-mm-dd` que propose **R** à travers la fonction `as.Date`{data-pkg=base}. Pour convertir la variable, on utilise l'extension `lubridate`{.pkg}, qui peut facilement interpréter les mois écrits en langue française grâce à l'argument `locale` spécifié ci-dessous : | ||
|
||
```{r, tidy = FALSE} | ||
d1$date = parse_date_time(d1$date, "%d %m %Y", locale = "fr_FR") %>% | ||
as.Date | ||
``` | ||
|
||
### Utilisation de la syntaxe XPath | ||
|
||
L'exemple que l'on vient de voir permet de récupérer les données du blog [Confusionnisme][d2]. Il se trouve que ce code fonctionne presque aussi bien pour le blog [Conspis hors de nos vi[ll]es][d3] : en effet, celui-ci utilisant aussi le moteur de blog WordPress, la structure de ses pages est quasiment identique à celle que l'on vient de voir. Voici le code complet pour récupérer les 5 pages de ce blog : | ||
|
||
```{r, tidy = FALSE, cache = TRUE} | ||
d2 = data_frame() | ||
for(i in 1:5) { | ||
h = html(paste0("http://conspishorsdenosvies.noblogs.org/page/", i)) | ||
d2 = rbind(d2, data_frame( | ||
url = html_nodes(h, "#content .entry-title a") %>% html_attr("href"), | ||
title = html_nodes(h, "#content .entry-title a") %>% html_text, | ||
date = html_nodes(h, "#content .entry-date") %>% html_text, | ||
tags = html_nodes(h, ".tag-links") %>% | ||
sapply(function(x) html_nodes(x, xpath = "a[@rel='tag']") %>% | ||
html_text %>% | ||
paste0(collapse = ";")) | ||
)) | ||
} | ||
d2$date = parse_date_time(d2$date, "%d/%m/%Y") %>% as.Date | ||
``` | ||
|
||
On remarquera que plusieurs petites choses ont changé : par exemple, sur le blog [Conspis hors de nos vi[ll]es][d3], les dates sont affichées dans un format `dd/mm/yyyy` qui ne nécessite pas de conversion, car chaque élément de la date est donné sous la forme d'un chiffre. On remarquera aussi que l'emplacement de la date a changé, car le gabarit graphique du blog diffère de celui de [Confusionnisme][d2] et place cette information dans un élément différent du [code source de la page](view-source:http://conspishorsdenosvies.noblogs.org/). | ||
|
||
Le changement le plus important ici concerne l'utilisation de la syntaxe [XPath](https://fr.wikipedia.org/wiki/XPath) : en effet, pour récupérer les mots-clés, il nous a fallu limiter ceux-ci à ceux se trouvant dans des hyperliens (`<a>`) dont la propriété `rel` est égale à `tag`, pour ne pas également récupérer les mots-clés correspondant à des catégories du blog. La syntaxe XPath est un peu plus alambiquée : ici, c'est l'expression `a[@rel='tag']` qui accomplit l'opération souhaitée, à condition d'être bien passée à l'argument `xpath` de la fonction `html_nodes`{data-pkg=rvest}. | ||
|
||
## Combinaison des résultats | ||
|
||
Il nous reste un blog à couvrir : [Conspiracy Watch][d1]. Le code pour celui-ci diffère assez fondamentalement des blogs précédents du point de vue de la syntaxe de ses pages, qui utilisent un moteur de blog complètement différent de WordPress. Après lecture de la source, on arrive au code suivant, qui récupère les mêmes variables que récupérées pour les deux autres blogs : | ||
|
||
```{r, tidy = FALSE, cache = TRUE} | ||
d3 = data_frame() | ||
for(i in seq(0, 1180, 20)) { | ||
h = html(paste0("http://www.conspiracywatch.info/?start=", i)) | ||
d3 = rbind(d3, data_frame( | ||
url = html_nodes(h, "#mod_1260437 .titre a") %>% html_attr("href"), | ||
title = html_nodes(h, "#mod_1260437 .titre") %>% html_text %>% str_trim, | ||
date = html_nodes(h, "#mod_1260437 .cel_pied .date") %>% html_text, | ||
tags = html_nodes(h, "#mod_1260437 .cel_pied") %>% | ||
sapply(function(x) html_nodes(x, ".objet-tag a") %>% | ||
html_text %>% | ||
paste0(collapse = ";")) | ||
)) | ||
} | ||
d3$url = paste0("http://www.conspiracywatch.info", d3$url) | ||
d3$date = parse_date_time(d3$date, "%d %m %Y", locale = "fr_FR") %>% as.Date | ||
``` | ||
|
||
Il ne reste plus qu'à combiner les différents résultats de nos récupérations, de les ordonner par date de publication, puis d'harmoniser les mots-clés a minima, en supprimant les traits d'union et en s'assurant qu'ils ne contiennent pas de lettres majuscules : | ||
|
||
```{r} | ||
d = rbind(d1, d2, d3) %>% arrange(date) | ||
d$tags = gsub("-", " ", d$tags) %>% tolower | ||
``` | ||
|
||
L'inspection du résultat montre que l'on dispose à présent d'un jeu de données contenant les métadonnées de 1,268 billets de blogs, dont l'immense majorité proviennent de [Conspiracy Watch][d1] : | ||
|
||
```{r} | ||
# nombre de billets récupérés | ||
nrow(d) | ||
# sources des billets | ||
table(substr(d$url, 1, 25)) | ||
``` | ||
|
||
Il ne reste plus qu'à sauvegarder ce résultat, pour réutilisation future : | ||
|
||
```{r} | ||
write_csv(d, "data/conspi.csv") | ||
``` | ||
|