Initiation au webscraping

Gabriel Alcaras

Présentation générale

Définitions & usages

Qu’est-ce que le webscraping ?

  • Extraire des informations d’une page web pour en faire un usage détourné.
  • Producteurs du site -> usage premier : poser une question, discuter avec des ami·e·s, partager un logiciel, archiver des photos, etc.
  • Usagers -> comment en faire un usage différent ?
  • Le webscraping est un des moyens d’en faire un usage détourné.

Quels usages détournés ?

Quand faut-il scraper ?

Méthode Avantages Inconvénients
Webscraping Furtif ; interface ; contrôle ; ludique Légalité floue ; complexité ; éphémère
API Légal si CGU ; données nettoyées ; requêtes simples ; durabilité Limites ; prix ; inscription ; pas d’interface
Accès direct à la base de données Légal ; informations nettoyées ; pas de programmation ! négociation ; pas de contexte / contrôle

Usages en sciences sociales

  • Visible pour les terrains en ligne
  • Invisible/invisibilisé dans d’autres recherches : complément d’information

Vision d’ensemble

Les grands principes du webscraping

Comment ça marche ?

  • Le webscraping tire parti d’un principe simple : les pages web sont générées par un processus automatique et déterministe
  • Les sites réutilisent la même structure et la même mise en forme pour afficher différentes informations
  • Par conséquent, en comprenant ce processus automatique et déterministe, alors nous pouvons automatiser la récupération de ces informations.

Deux corollaires

  1. Moins le processus de génération d’un site est déterministe (écriture manuelle, génération aléatoire de contenu comme dans les pages personnalisées ou la vente de billets d’avion), moins le webscraping est efficace.
  2. Pour écrire un bon webscraper, il faut comprendre un minimum d’éléments sur la production de sites web.

Oui, mais comment ça marche ?

En pratique, notre bot va procéder en deux temps :

  1. Le scraper : extraire les informations sur un certain type de page (email d’une archive, liste de tweets, page d’un topic sur un forum)
  2. Le crawler : naviguer de lien en lien pour pointer le scraper vers toutes les pages pertinentes (listes des emails dans un thread, liste de profils, listes de threads d’un forum)

Crawler / scraper

Comment se représenter un scraper ?

  1. Comme un robot qui naviguerait sur un site très très rapidement, comme si nous le faisions avec notre navigateur.
  2. Il peut être utile de tirer profit du fait que notre robot ne “voit” pas exactement ce que nous voyons.
  3. Dans certains cas, il peut être souhaitable que notre robot se comporte de manière moins “robotique”.

Quelles sont les grandes étapes ?

  1. Concevoir un scraper minimal (récupère une information sur une page)
  2. Puis concevoir le crawler qui va donner les pages pertinentes au scraper
  3. Améliorer le crawler (stockage de données, etc.)
  4. Enrichir le scraper avec des informations supplémenaires

Quel langage pour faire du scraping ?

  • Le langage mal adapté qu’on connaît mal vaut toujours mieux qu’un langage parfait qu’on ne connaît pas
  • R évidemment (paquet rvest) -> bien pour débuter, scraping de base.
  • Python (mechanize, scrapy) -> pour les projets les plus ambitieux.
  • Selenium ? -> pour les cas désespérés.

Quelles connaissances sont nécessaires ?

  • Débutant : HTML
  • Débutant : CSS
  • Débutant : XPath (1.0 pour rvest, 2.0 pour Python et cie)
  • Intermédiaire : REGEX
  • Intermédiaire : requêtes POST
  • Intermédiaire : HEADERS
  • Intermédiaire : cookies
  • Avancé : Javascript

Notre exemple : Allociné

Pourquoi Allociné ?

  • Projet de recherche plausible : espace des critiques cinématographiques
  • Un projet d’ampleur raisonnable : collecter toutes les notes de films sur Allociné
  • Le site Allociné est (plutôt) simple à scraper

La page cible (scraper)

Page cible : les notes presse

L’index (crawler)

Page index

Et concrètement ?

  • Tous les films parus entre 2000 et 2020 (avec une note de presse)
  • soit 11359 films
  • et 174222 notes de presse
  • récoltées en 4.5 heures
  • avec 23475 requêtes, soit 1.5 requêtes par seconde.

Distribution des évaluations (étoiles)

Au fil du temps

Selon la revue

Dans le temps, par revue

De proche en proche

Le paquet Socscrap

Que contient Socscrap ?

  • Un scaper Allociné prêt à l’emploi : get_ratings(2017)
  • Des fonctions intermédiaires : get_film_metadata(url)
  • Des fonctions générales : get_text(".class")
  • Les données collectées disponibles : data(ratings)
  • Statistiques d’extraction : data(report)
  • Quelques vignettes explicatives : vignette("allocine-scraper")
  • Le code disponible sur : gaalcaras/socscrap

Comment ça marche ?

  • 6 fonctions, du plus haut niveau (get_ratings) au plus bas (get_text)
  • Aide disponible pour chacune : ?get_ratings
  • Pour s’entraîner :
    • De haut en bas : commencer par utiliser get_ratings, puis la coder soi-même ; puis remplacer process_filmlist ; puis get_film_ratings
    • De bas en haut : en s’aidant du code du paquet

Testez le !

get_ratings(2017, pages = 4)
#> # A tibble: 94 x 9
#>    paper  rating  title date       duration genre nationality direction actors  
#>    <chr>  <chr>   <chr> <date>        <dbl> <chr> <chr>       <chr>     <chr>   
#>  1 Bande… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  2 Cinem… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  3 Ecran… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  4 L'Exp… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  5 Le Po… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  6 Paris… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  7 Posit… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  8 Trans… Chef-d… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#>  9 20 Mi… Très b… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#> 10 Closer Très b… A Be… 2017-11-08       90 Drame britannique De Lynne… Avec Jo…
#> # … with 84 more rows

Premiers pas

La page cible (scraper)

https://www.allocine.fr/film/fichefilm-215099/critiques/presse/

Page cible : les notes presse

À quoi ressemble le scraper ?

"https://www.allocine.fr/film/fichefilm-215099/critiques/presse/" %>%
  get_film_ratings()
#> # A tibble: 40 x 9
#>    paper  rating  title  date       duration genre nationality direction actors 
#>    <chr>  <chr>   <chr>  <date>        <dbl> <chr> <chr>       <chr>     <chr>  
#>  1 Bande… Chef-d… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  2 Closer Chef-d… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  3 IGN F… Chef-d… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  4 Les I… Chef-d… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  5 Mad M… Chef-d… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  6 Télé … Chef-d… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  7 20 Mi… Très b… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  8 CNews  Très b… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#>  9 Cinem… Très b… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#> 10 Derni… Très b… Star … 2017-12-13      152 Acti… américain   De Rian … Avec D…
#> # … with 30 more rows

Que fait le scraper ?

get_press_ratings

"https://www.allocine.fr/film/fichefilm-215099/critiques/presse/" %>%
  get_press_ratings()
#> # A tibble: 40 x 2
#>    paper                        rating       
#>    <chr>                        <chr>        
#>  1 Bande à part                 Chef-d'oeuvre
#>  2 Closer                       Chef-d'oeuvre
#>  3 IGN France                   Chef-d'oeuvre
#>  4 Les Inrockuptibles           Chef-d'oeuvre
#>  5 Mad Movies                   Chef-d'oeuvre
#>  6 Télé Loisirs                 Chef-d'oeuvre
#>  7 20 Minutes                   Très bien    
#>  8 CNews                        Très bien    
#>  9 CinemaTeaser                 Très bien    
#> 10 Dernières Nouvelles d'Alsace Très bien    
#> # … with 30 more rows

get_film_metadata

"https://www.allocine.fr/film/fichefilm_gen_cfilm=215099.html" %>%
  get_film_metadata()
#> # A tibble: 1 x 7
#>   title        date       duration genre  nationality direction  actors         
#>   <chr>        <date>        <dbl> <chr>  <chr>       <chr>      <chr>          
#> 1 Star Wars -… 2017-12-13      152 Action américain   De Rian J… Avec Daisy Rid…

Page cible secondaire : fiche film

Fiche film

L’inspecteur

Que voit-on dans la console ?

  • À peu près tous les échanges entre le navigateur et le serveur :
    • Requêtes : HTTP (GET, POST, HEADERS) -> onglet Network
    • Affichage d’une page : texte, HTML, CSS
    • Stockage client : cookies -> Network
    • Code dynamique : Javascript -> Network, Console
  • Le code est interprété / exécuté par le navigateur

Afficher une page web

  • Le texte de la page lui-même
  • HTML (HyperText Markup Language) : langage dit « de balisage » (markup language)
    • annoter un texte avec des informations supplémentaires
    • usage canonique : décrire la structure (en-tête, paragraphe) ou bien sa sémantique (emphase, citation)
    • en pratique : aussi utilisé pour la mise en page
  • CSS (Cascading StyleSheets) : mise en page (position, couleur, arrière-plan)

Manipuler une page web

  • Manipuler ces pages web pour interagir avec les internautes
  • Implique une communication avec le serveur
  • HTTP (HyperText Transfer Protocol) :
    • protocole de communication
    • voir une page : HTTP GET
    • remplir un formulaire de contact : HTTP POST
  • Javascript : langage de programmation -> aspect dynamique des pages (chat, masquage, infinite scroll, …)

HTML : code

Balises imbriquées :

<div>
  <p>Liste à puces</p>
  <ul>
    <li>Élément 1</li>
    <li>Élément 2</li>
  </ul>
</div>

HTML : arborescence

Balises = nœud (node)

HTML : XPATH

//div/ul/li

CSS

<div>
  <p>Liste à puces</p>
  <ul>
    <li class="rouge">Élément 1</li>
    <li class="jaune">Élément 2</li>
  </ul>
</div>

En pratique

  • Exercice : récupérer la date de sortie du film
  • Utiliser l’inspecteur
  • Puis la fonction get_text() (de soscrap)

Solution

"https://www.allocine.fr/film/fichefilm_gen_cfilm=215099.html" %>%
  xml2::read_html() %>%
  get_text(".date")
#> [1] "13 décembre 2017"

Paquets utiles

Tidyverse

rnorm(100) %>%
  mean()
#> [1] -0.03102111
  • Un ensemble de paquets qui viennent redéfinir l’usage de R : dplyr, forcats, stringr, lubridate, …
  • Systématiser et optimiser de nombreux aspects de R
  • Le symbole %>% (pipe) qui devient vite indispensable (paquet magrittr)

Here

read_csv(here::here("data", "enquete_emploi.csv"))
  • Chemin toujours relatif à la racine du projet (projet RStudio, paquet R, dépôt git, …)
  • Code compatible avec tous les systèmes d’exploitation (Linux, macOS, windows, …)
  • Plus de setwd(), enfin des chemins reproductibles !

Glue

library(glue)

glue("Adieu", "horrible", "paste()", .sep = " ")
#> Adieu horrible paste()

a <- 41

glue("La réponse à votre question est {a+1}.")
#> La réponse à votre question est 42.
  • Adieu, horribles fonctions paste() et collapse()
  • Enfin une fonction dans R pour formater des variables simplement (fstrings dans Python 3.8, printf chez UNIX, …)

Rvest et xml2

  • Des fonctionnalités rudimentaires mais suffisantes pour faire un scraper simple dans R

Écrire des fonctions dans R

Faire ses propres fonctions, à quoi bon ?

  • Factorisation :
    • Économie de moyens : au lieu de faire des copier-coller
    • Reproductible : utiliser une fonction dans une boucle
    • Maintenance : on peut changer le comportement du code en modifiant seulement une fonction
  • Lisibilité :
    • Au lieu de (trop) commenter, écrire du code qui parle de lui-même
    • Bénéficier d’un nom de fonction (un verbe) évocateur
    • Regrouper des lignes dans un ensemble cohérent
    • Cacher de la complexité inutile

Rappels

nom_de_la_fonction <- function(arg1, arg2 = FALSE) {
  result <- arg1
  
  if(arg2) {
    result <- arg1 + arg2
  }
  
  # Le retour de la fonction est implicite dans R
  result
}

nom_de_la_fonction(3)
#> [1] 3

Écrire de meilleures fonctions

  • Informer : utiliser message() plutôt que print()
  • Arrêter : en cas d’erreur, stop() avec un message d’erreur clair
  • Plusieurs retours : return() pour forcer un retour
  • Vérifier les arguments :
    • rlang::is_missing() pour les arguments normaux
    • Avancé : si tidyeval, rlang::quo_is_missing()
  • Écrire des tests unitaires : voir le paquet testthat

Exemple

nom_de_la_fonction <- function(arg1, arg2 = FALSE) {
  if(rlang::is_missing(arg1)) {
    return(NA)
    stop("Vous avez oublié de donner 'arg1'!")
  }
  
  result <- arg1
  
  if(arg2) {
    message("On ajoute arg2")
    result <- arg1 + arg2
  }
  
  result
}

Programmation fonctionnelle

Ou comment j’ai appris à ne plus m’en faire et à aimer les boucles

Rappel : les boucles dans R

pages <- c(1, 2, 3)
films <- tibble(
  titre = character(),
  real = character(),
)

for (page in pages) {
  films <- films %>%
    add_row(tibble(titre = glue("Film {page}"),
                   real = glue("Real {page}"))
            )
}

Qu’est-ce que j’obtiens si je fais print(films) ?

Rappel : les boucles dans R

pages <- c(1, 2, 3)
films <- tibble(
  titre = character(),
  real = character(),
)

for (page in pages) {
  films <- films %>%
    add_row(tibble(titre = glue("Film {page}"),
                   real = glue("Real {page}"))
            )
}

print(films)
#> # A tibble: 3 x 2
#>   titre  real  
#>   <glue> <glue>
#> 1 Film 1 Real 1
#> 2 Film 2 Real 2
#> 3 Film 3 Real 3

Le problème avec les boucles

for(annee in annees) {
  for(page in pages) {
    for(film in films) {
      for (note in notes) {
        return(toutes_les_notes)
      }
      films %>%
        add_row(...)
    }
  }
  return(films %>%
           add_column(annee = annee))
}

Quels problèmes ?

Le problème avec les boucles

for(annee in annees) {
  for(page in pages) {
    for(film in films) {
      for (note in notes) {
        return(toutes_les_notes)
      }
      films %>%
        add_row(...)
    }
  }
  return(films %>%
           add_column(annee = annee))
}

Quels problèmes ?

  • “Enfer de l’indentation” (lisibilité)
  • Incertitude sur l’action de la boucle

Des boucles…

pages <- c(1, 2, 3)
films <- tibble(
  titre = character(),
  real = character(),
)

for (page in pages) {
  films <- films %>%
    add_row(tibble(titre = glue("Film {page}"),
                   real = glue("Real {page}"))
            )
}

print(films)
#> # A tibble: 3 x 2
#>   titre  real  
#>   <glue> <glue>
#> 1 Film 1 Real 1
#> 2 Film 2 Real 2
#> 3 Film 3 Real 3

Aux cartes !

library(purrr)

pages <- c(1, 2, 3)
films <- map_df(pages, ~ tibble(titre = glue("Film {.}"),
                                real = glue("Real {.}"))
                )

print(films)
#> # A tibble: 3 x 2
#>   titre  real  
#>   <glue> <glue>
#> 1 Film 1 Real 1
#> 2 Film 2 Real 2
#> 3 Film 3 Real 3

Autres exemples

map_chr(films$real, ~ str_extract(., "\\d"))
#> [1] "1" "2" "3"

map_int(films$real, ~ as.integer(str_extract(., "\\d")))
#> [1] 1 2 3

map2_chr(films$titre, films$real, ~ glue("{.x} par {.y}"))
#> [1] "Film 1 par Real 1" "Film 2 par Real 2" "Film 3 par Real 3"

Avantages de purrr

  • Adieu à l’enfer de l’indentation !
  • Le code peut s’écrire linéairement plutôt qu’en imbrication
  • Le nom de la fonction indique ce qu’elle retourne :
    • map : une liste
    • map_int : un vecteur d’integers
    • map_chr : un vecteur de texte
    • map_df : une base de données type tibble

Exemple d’utilisation dans le code

# Définir une fonction pour récupérer les évaluations des films
# d'une page à partir de son URL
get_ratings_from_page <- function(url) { ... }

# Récupération des URLs des pages des films de cette année
this_year_urls <- ...

# Lancer le scraping et retourner une base de données toute faite :)
this_year_ratings <- map_df(this_year_urls,
                            ~ get_ratings_from_page(.x))

Caveat : que faire en cas d’erreur ?

urls <- c("https://google.com", "http://je-nexiste-pas.lol")

# Je récupère les nœuds de chaque page
resultat <- map(urls,
                ~ httr::GET(url) %>%
                  read_html() %>%
                  get_nodes())
#> Error in get_nodes(.): could not find function "get_nodes"

print(resultat)
#> Error in print(resultat): object 'resultat' not found

Solution

urls <- c("https://google.com", "http://je-nexiste-pas.lol")

# Je récupère les nœuds de chaque page, en retournant des NA en cas d'échec
resultat <- map_chr(urls,
                    possibly(~ read_html(url) %>%
                           html_text(),
                         NA_character_
                ))

print(resultat)
#> [1] NA NA