Web scraping avec R
Cet article est en construction: un auteur est en train de le modifier.
En principe, le ou les auteurs en question devraient bientôt présenter une meilleure version.
Introduction
R permet d'importer différents types de fichiers et par conséquent il est possible d'importer aussi des pages web. Cependant, avec ce type d'importation, tout le contenu de la page (i.e. tout le code HTML) est importé, ce qui n'est pas souvent le comportement souhaité car on s'intéresse seulement à une partie (ou des parties) spécifique(s) du document. À ce point, il faudrait donc faire recours à des fonctions de traitement alphanumériques pour aspirer/nettoyer les parties d'intérêt du document. Cette opération peut se reveler plutôt compliquée, surtout si elle nécessite l'utilisation des expressions régulières. À ce propos, ce tutoriel illustre le fonctions de la bibliothèque rvest afin de faciliter le web scraping avec R, c'est-à-dire mettre à disposition des puissantes fonctionnalités de R des données tirées des pages web.
- Pour un aperçu général sur la technique d'extraction de données depuis des pages web, consulter la page web scraping disponible dans ce wiki.
Identifier les éléments d'intérêt dans une page
Avant de présenter la bibliothèque, il peut être utile d'illustrer brièvement quelques pratiques pour identifier des éléments dans une page dont on veut extraire les données. À ce propos, la plupart des navigateurs web propose non seulement la possibilité de voir le code source d'une page, mais aussi d'analyser les éléments.
Cette fonction est normalement disponible directement dans le menu contextuel du navigateur (i.e. click droit). Il suffit par conséquent de pointer sur l'élément qui nous intéresse, cliquer le bouton droit du mouse, et choisir l'item du menu contextuel qui permet d'analyser l'élément. Une console du navigateur s'ouvrira et selon le type de navigateur elle proposera des informations sur l'élément. Voici une capture d'écran de FireFox qui résulte de l'analyse du tableau des contributeurs dans la page d'accueil de EduTech Wiki en français.
À travers ces informations on peut déterminer les meilleurs critères pour identifier l'élément. Voici une liste de ces critères présentés selon leur "facilité" d'identification :
- Attribut id : si l'élément présente un attribut id (caractérisé dans l'identificateur avec le caractère #), cet attribut est censé être unique à l'élément. Par conséquent, ce type d'identificateur est très utile pour extraire un élément spécifique de la page, mais ce n'est pas indiqué pour extraire des éléments récursifs (e.g. plusieurs paragraphes), car chacun devrait avoir un id unique. L'intérêt de l'identificateur id dans une extraction récursive est souvent d'identifier l'élément "conteneur" des éléments récursifs à extraire (e.g. un div de id="content" permet souvent d'isoler le contenu de la page de l'entête et du menu de navigation généraux du site)
- Classes : les classes (caractérisées par le caractère .) représente un bon moyen d'identifier des éléments récursifs présents au même niveau hiérarchique. Par exemple les commentaires d'un blog partage souvent la même classe (e.g. class="comment"). Pour maximiser la probabilité d'identifier le bon élément, dans le cas que plusieurs éléments à différents endroits dans la page présente la même classe, ce critère peut être associé au nom de la balise (e.g. div.comment) et/ou à sa position hiérarchique dans la page (voir point suivant)
- Position hiérarchique : l'analyse des éléments propose souvent la position relative à la racine du DOM occupé par l'élément sélectionné. Cette manière d'identifier les éléments est celle qui présente le plus de risque de ce tromper ou de trouver plusieurs éléments qui partages le même type de hiérarchie relative. Elle devrait être utilisé "en solitaire" seulement si les balises ne présentent jamais des attributs de type id ou class.
Voir une liste exhaustive des sélecteurs CSS
La bibliothèque rvest
La bibliothèque rvest permet d'extraire du contenu des pages web à l'aide de la syntaxe XPath ou des sélecteurs CSS. Surtout les sélecteurs CSS représentent un outil accessible car ils sont utilisés fréquemment dans le développement des pages web. Cette notation combine deux critères d'identification :
- La structure hiérarchique du DOM en termes d'emboitement des balises. Par exemple, un paragraphe qui se trouve directement à l'intérieur d'une balise de type
div
peut être identifié à l'aide de la notationdiv > p
- Des identificateurs trans-hiérarchiques comme les classes, les identificateurs uniques, et les attributs. Par exemple un paragraphe auquel on a attribué la class "important" peut être identifié à l'aide de la notation
p.important
Les deux critères peuvent s'appliquer de manière combinée, c'est-à-dire qu'il est possible d'identifier des éléments, caractérisée par des indicateurs trans-hiérarchiques, selon leur positionnement hiérarchique relatifs à la structure du document. La combinaison des deux exemples illustrés plus donnerait ceci : div > p.important
. Cette notation se traduit par l'identification de paragraphes avec une classe de type "important" qui se trouvent directement à l'intérieur d'une balise div
.
Cette notation permet de créer des critères de manière très flexible, car elle met à disposition un large éventail d'ancrages dans une page web pour repérer les informations d'intérêt. De plus, cette notation très simple à l'apparence, peut se complexifier et créer des critères de selection très précis, même si les expressions régulières restent la méthode la plus puissante.
- Voir une liste exhaustive des sélecteurs CSS
Installation de la bibliothèque
Pour installer la bibliothèque rvest il suffit de lancer la commande R suivante :
install.packages("rvest")
Utilisation de la bibliothèque
En considération du fait que la bibliothèque ne fait pas partie des bibliothèques standard disponibles en tout moment dans R, elle devra être "chargée" à chaque nouvelle instance d'utilisation de R avec la commande :
library(rvest)
Fonctions disponibles
rvest est une bibliothèque assez simple, qui ne présente pas beaucoup de fonctions, mais qui met à disposition les fonctionnalités principales nécessaires à l'identification et extraction des données dans une page, ainsi que quelques fonctions supplémentaires qui permettent de naviguer les pages en émulant un navigateur web. Voici la liste de fonctions qui seront approfondies dans cette page :
- html()
- html_nodes()
- html_text(), html_attrs(), html_tag()
- html_table()
D'autres fonctions permettant de naviguer ou d'interagir avec des formulaires web sont également disponibles. Voir la documentation officielle pour plus de détails.
Fonction html()
La fonction html() est généralement la première à être utilisé dans un flux d'extraction car elle permet d'importer en R le contenu d'une page web. La fonction accepte donc deux paramètres, dont le deuxième (encoding) est optionnel. :
html(x, encoding=NULL)
Le paramètre x sert à identifier la ressource contenant du code HTML. Cette ressource peut être déclarée de trois manières :
- URL : la ressource est identifiée par son adresse web, par exemple html(http://edutechwiki.unige.ch/fr/Accueil)
- Fichier local : la ressource est identifié par le nom d'un fichier local. Cette deuxième modalité est utile dans le cas que vous ne puissiez par accéder à votre ressource sans un mécanisme d'authentification. Dans ce cas vous pouvez sauvegarder la page depuis votre navigateur. La manière plus simple pour accéder à un fichier local est de changer le répertoire de travail de R (à travers le menu File >) et de le faire pointer au dossier qui contient votre page (ou vos pages) HTML. À ce point, il vous suffit de déclarer le nom du fichier. Par exemple html("index.html").
- HTML "brut" : la ressource se compose directement du code HTML passé comme une suite de caractères. Notez à ce propos que la fonction s'attende à un document HTML complet. Si votre HTML ne présente pas la structure minimal d'un document HTML, rvest va la générer automatiquement.
En ce qui concerne le deuxième paramètre, il permet d'encoder la page selon un codage de caractères spécifiques (e.g. UTF-8). Pour régler des problèmes de codage la bibliothèque met aussi à disposition les fonctions guess_encoding() et repair_encoding(), mais l'utilisation de l'argument encoding directement dans la fonction html(x, encoding = "UTF-8")
résout normalement le problème à la base.
Voici un example d'utilisation qui associe à la variable page le contenu de la homepage EduTechWiki en français :
page <- html("http://edutechwiki.unige.ch/fr/Accueil")
Pour contrôler que tout a bien fonctionné, vous pouvez voir le contenu de la variable simplement en saisissant son nom.
page
Vous devrez obtenir un résultat similaire à celui ci :
Fonction html_nodes()
La fonction html_nodes() représente la "vraie" fonction de web scraping, car elle permet d'extraire des morceaux de code HTML contenant les informations d'intérêt à partir d'une page importée par la fonction html(). Pour extraire les données, html_nodes() met à disposition deux moyens :
- XPath : standard W3C pour identifier des informations à l'intérieur d'un document XML
- Les sélecteurs CSS : patterns pour identifier des éléments dans une page HTML.
En réalité, la bibliothèque utilise concrètement XPath pour extraire le contenu. En effet les sélecteurs CSS sont automatiquement transformés par rvest en syntaxe XPath.
La fonction html_nodes() accepte deux arguments, les deux obligatoires :
html_nodes(x, [css, xpath])
L'argument x représente du code HTML, notamment une page importée avec la fonction html().
Le deuxième argument est un critère de sélection à l'intérieur du code passé en référence dans l'argument x. Voici un exemple pour extraire les paragraphes avec les sélecteurs CSS à partir du code HTML préalablement associé à une variable "page":
html_nodes(page, "p")
rvest support la majorité des sélecteurs de type CSS3, les exception sont spécificées dans la documentation officielle de la bibliothèque.
Si vous préférez utiliser la syntaxe XPath, il faut le déclarer explicitement dans le deuxième argument. Voici le même exemple avec XPath:
html_nodes(page, xpath="//p")
La fonction html_nodes() renvois une liste (tableau) des occurrences trouvées dans le code analysés en fonction des critères de sélection spécifiée. Le résultat de cette fonction est généralement traité ultérieurement avec les fonctions html_text().
La fonction html_node() (sans s finale) est également disponible. Son fonctionnement est le même, sauf pour le fait que seulement la première occurrence sera retenue.
Fonctions html_text(), html_attrs(), et html_tag()
Toutes ces fonctions servent pour aspirer/nettoyer "définitivement" les éléments d'intérêt que nous avons isolés à travers la fonction html_nodes() :
- html_text(x, ...) : enlève toutes les balises du code isolé et affiche seulement le contenu textuel. La fonction accepte un argument obligatoire, c'est-à-dire le code qui doit être "nettoyé" ainsi que des arguments facultatifs dont le plus utile est trim = TRUE pour enlever des espaces avant et après le texte, e.g. html_text(code, trim = TRUE)
- html_attrs(x) : identifie les attributs des balises présents dans le code x (disponible aussi html_attr() pour un seule attribut dont il faut spécifier le nom)
- html_tag(x) : identifie le type de balises présent dans le code x
Fonction html_table()
La fonction html_table(), comme le nom le suggère, est spécialement conçue pour extraire facilement le contenu d'un tableau HTML, en gardant la structure en lignes et colonnes, ce qui permet entre autres d'exécuter successivement des analyses statistiques avec R.
La fonction accepte 5 paramètres dont seulement le premier est obligatoire :
html_table(x, header = NA, trim = TRUE, fill = FALSE, dec = ".")
Le paramètre x fait référence à le code HTML du tableau. Header détermine si la première ligne du tableau doit être utilisée en tant que label des données, et donc ne pas faire partie des analyses sur les données. Si cette option n'est pas spécifiée (avec une valeur TRUE ou FALSE), la fonction va créer une entête seulement si le tableau présente un ligne de type th
. Trim détermine si des éventuels espaces avant/après les données doivent être supprimés. Fill est une option utile dans le cas que où les tableaux ne présentent pas toujours le même nombre de colonnes par ligne (ce qui peut s'avérer avec l'attribut HTML colspan
). Si cette option est spécifiée TRUE, les lignes avec moins de colonnes que prévu seront "filled" automatiquement. L'option dec, enfin, détermine quel caractère utiliser pour séparer les chiffres décimales.
Séquence des commandes dans la Console R
L'extraction de contenu avec rvest se fait généralement en trois étapes :
- Importer la page en R avec la fonction html()
- Identifier la partie de la page qui contient les données avec la fonction html_nodes()
- Aspirer/Nettoyer le contenu avec l'une de ces fonctions : html_text(), html_attrs(), html_tag(), ou html_table()
Ce processus itérative peut être enchaîné dans la Console R en trois manières différentes :
- Déclaration de variables : déclarer à chaque passage une nouvelle variable et la passer en argument dans l'étape successive
- Emboitement des fonctions : condenser les trois actions en une seule commande avec emboîtement des fonctions
- Enchaînement des fonctions : Condenser les trois actions en une seule commande à l'aide de la notation %>% qui permet d'enchaîner les commandes
De suite, chaque manière sera proposé avec la même opération qui consiste à extraire le texte du titre de la page d'accueil de EduTech Wiki en français. Le code affiché présuppose que la bibliothèque rvest ait déjà été installé et chargée dans la session de travail de R(voir plus haut dans la page).
Déclaration de variables
Ce processus implique trois passages.
1. Créer une variable "page" avec le contenu HTML du document.
page <- html("http://edutechwiki.unige.ch/fr/Accueil")
2. Créer une variable "titre" qui identifie la balise title
à l'intérieur de la variable "page" passé en référence. Il n'y a qu'une balise de ce type dans une page, donc on utilisera la fonction html_node() plutôt que html_nodes()
titre <- html_node(page, "title")
3. Créer une variable "texte" qui enlève, à travers la fonction html_text() avec la variable "titre" passé en référence, les balises <title>...</title>
.
texte <- html_text(titre)
À ce point, si on demande d'imprimer à l'écran le contenu de la variable texte, on obtiendra :
[1] "EduTech Wiki"
Emboitement des fonctions
Le même résultat peut être obtenu avec une seule ligne de commande qui "imbrique" les trois fonctions.
html_text(html_nodes(html("http://edutechwiki.unige.ch/fr/Accueil"), "title"))
Cette notation a l'avantage de tenir sur une seule ligne, mais elle augmente la possibilité de se tromper avec le nombre des parenthèses ou la bonne répartition des paramètres. L'enchaînement des fonctions présenté de suite permet de surmonter ce problème.
Enchaînement des fonctions
Grâce à la notation %>% on peut étaler les trois passages en succession, tout en gardant une seule ligne de commande. Voici la notation à utiliser pour le même exemple :
html("http://edutechwiki.unige.ch/fr/Accueil") %>% html_nodes("title") %>% html_text()
Exemples d'extraction
De suite quelques simples exemples d'extraction de données avec la bibliothèque rvest seront présentés. La plupart de ces exemples concerne des pages disponibles dans ce wiki, ainsi que les exemples puissent être testés facilement. Deux aspects sont cependant important à ce sujet :
- la structure des pages wiki peut changer facilement dans le temps, donc certains exemples pourraient donner des outputs différents ou même ne plus marcher du tout à cause des ces changements (voir la section sur les limites techniques du web scraping pour plus de détails);
- pour une "vraie" extraction de données de ce wiki, il faudrait considérer la possibilité d'utiliser l'API mise à disposition.
NB: les exemples illustrés de suite présupposent que la bibliothèque rvest ait déjà été installée et chargée dans la session de travail de R (voir plus haut dans la page).
Extraire les URL des liens d'une page web
Dans ce premier exemple, nous allons extraire les liens d'une page de l'article "Web scraping" de EduTech Wiki en français. Nous allons d'abord extraire tous les liens, ensuite seulement les liens externes à la page (donc sans par exemple les raccourcis du sommaire), et enfin tous les liens externe au Wiki (donc sans les liens à d'autres pages du Wiki).
Commençons d'abord pour importer le code de la page et l'associer à la variable "page". Cette variable sera utilisée dans les trois extractions, il est donc bien de l'associer à une variable ainsi que chaque extraction ne requiert pas une nouvelle requête au serveur.
page <- html("http://edutechwiki.unige.ch/fr/Web_scraping")
Extraire tous les liens
Même si on est intéressés à tous les liens, en réalité on est intéressés seulement aux liens qui font partie du corpus de l'article. Donc il faudra identifier dans la page wiki seulement le contenu de l'article. Une analyse de la structure de la page Wiki nous suggère que le contenu d'un article est facilement identifiable à travers l'attribut id="content". Nous sommes donc intéressés à tous les balises a
présent à l'intérieur de cet élément.
html_nodes(page, "#content a") %>% html_attr("href")
La variable all_urls contient maintenant une liste tous les attributs "href" des balises a
présents dans le texte de l'article. Vous pouvez notez que la liste comprend aussi des liens qui n'ont pas d'attribut "href" (e.g. NA). Pour les enlever, on peut spécifier dans la selection l'attribut href :
html_nodes(page, "#content a[href]") %>% html_attr("href")
Extraire les liens à d'autres pages
Pour extraire seulement les liens à une page externe, il faut spécifier un nouveau critère de sélection. Les ancrages internes se qualifient par le # au début du lien. On veut donc des liens qui ne commence pas avec ce caractère. Pour ce faire, on utilise le sélecteur CSS :not :
html_nodes(page, "#content a[href]:not([href^='#'])") %>% html_attr("href")
La variable page_urls ne contient plus les ancrages internes, mais seulement des liens à des pages externes.
Selon logique, si on voulait par contre retenir seulement les ancrages internes, il suffirait d'enlever le sélecteur :not :
html_nodes(page, "#content a[href][href^='#']") %>% html_attr("href")
Extraire des liens externes à EduTech Wiki
Enfin, nous voulons trouver seulement des références à des pages qui ne font pas partie de EduTech Wiki (ni anglais, ni français). Une possibilité serait d'exploiter le caractère la selection "a[href^='http']" parce que tout lien externe nécessite de commencer avec le protocol. Cependant, il y a la possibilité que des liens qui commencent avec http amènent quand même à l'intérieur du EduTech Wiki, soit à la version anglaise, soit parce que quelqu'un a copié/collé l'URL complet d'une page plutôt che utiliser le lien interne. On pourrait donc essayer d'exclure tout les liens qui contiennent le domaine "edutechwiki.unige.ch", mais ceci n'exclurait pas tous les liens internes à la page qui sont sous forme relative. Il faudrait donc combiner les deux critères. Cependant, en analysant mieux la structure de la page, on s'aperçoit que les liens externes reçoivent automatiquement un attribut class="external". À ce point on peut combiner cette classe avec le critère du domaine pour obtenir effectivement les liens externes :
html_nodes(page, "#content a.external:not([href*='edutechwiki.unige.ch'])") %>% html_attr("href")
Ce critère obtient le même résultat du plus complexe critère :
html_nodes(page, "#content a[href^='http']:not([href*='edutechwiki.unige.ch'])") %>% html_attr("href")
Cet exemple met en évidence l'importance de bien analyser la structure du code avant de choisir comment le sélectionner. Une vue d'ensemble plus méticuleuse avant de commencer le "scraping" peut faire épargner du temps et simplifier les critères d'extraction.
Extraire des éléments d'une liste "imbriquée"
Dans cet exemple, nous allons extraire les noms des catégories principales de ce wiki depuis la page d'accueil. L'intérêt de cet exemple consiste à extraire seulement les catégorie "de premier niveau" et pas toutes les sous-catégories.
En premier, il faut importer la page principale en R :
page <- html("http://edutechwiki.unige.ch/fr/Accueil")
L'analyse de la structure des éléments qui contiennent le nom de la catégorie permet de savoir que chaque nom de catégorie est un lien avec un attribut class ="CategoryTreeLabel". Nous allons donc utiliser cet indicateur pour extraire le texte des liens a avec cette classe.
html_nodes(page, "a.CategoryTreeLabel") %>% html_text()
Nous nous apercevons tout de suite qu'avec ce critère d'extraction nous avons effectivement obtenu les noms des catégories, mais non seulement celle de premier niveau. En effet, même les sous-catégories sont présentées comme des liens avec l'attribut class="CategoryTreeLabel" et correspondent donc au critère d'extraction. De plus, on s'aperçoit que les catégories de premier niveau ne présente pas des attributs différents par rapport aux sous-catégories. Pour les distinguer, il faudra donc trouver un critère de type hiérarchique.
On remonte donc dans la structure du DOM jusqu'à trouver le premier élément parent non partagé entre catégories de premier niveau et sous-catégories. Voici les deux parcours "en arrière" dans le DOM :
Catégorie | Lien avec nom | Niveau -1 | Niveau -2 | Niveau -3 |
---|---|---|---|---|
Catégorie principale | a.CategoryTreeLabel | div.CategoryTreeItem | div.CategoryTreeSection | div.CategoryTreeTag |
Sous-catégorie | a.CategoryTreeLabel | div.CategoryTreeItem | div.CategoryTreeSection | div.CategoryTreeChildren |
On constate donc que la première différentiation entre les deux types d'éléments se trouve au "3ème niveau supérieur".
Pour identifier seulement les catégories de premier niveau, il est donc suffisant de spécifier ces trois niveaux dans l'ordre hiérarchique correcte avant le lien de class="CategoryTreeLabel" :
html_nodes(page, ".CategoryTreeTag > .CategoryTreeSection > .CategoryTreeItem > .CategoryTreeLabel") %>% html_text()
Exemples d'intégration avec fonctionnalités de R
À faire
Liens
- Documentation de la bibliothèque (PDF)
- rvest: easy web scraping with R (Tutoriel base)
- Le projet de la bibliothèque sur GitHub (utile en cas de changements/mis à jour)