knitr::opts_chunk$set(
  warning = FALSE,
  message = FALSE,
  collapse = TRUE,
  fig.align = "center",
  comment = "#>"
)
library(tidyverse)
library(socscrap)
library(ggplot2)
library(glue)
library(thematic)
library(lubridate)
library(viridis)

data(ratings)
data("report")

ratings <- ratings %>%
  filter(!is.na(rating)) %>%
  mutate(year = year(date)) %>%
  filter(year >= 2000)

keep_main_papers <- function(.db, n = 25) {
  main25 <- ratings %>%
    count(paper, sort = T) %>%
    head(n) %>%
    pull(paper)

  .db %>%
    filter(paper %in% main25)
}

thematic_on(bg = "#1d1f21", fg = "#c5c8c6", accent = "#c5c8c6")
crossbar_col <- "#373b41"

Présentation générale

Définitions & usages

Qu'est-ce que le webscraping ?

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

Vision d'ensemble

Les grands principes du webscraping

Comment ça marche ?

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

DiagrammeR::grViz('
digraph {
  bgcolor = "#1d1f21";
  rankdir = LR;

  edge[color = "#c5c8c6", arrowsize = 0.5]

  node[shape = circle, fixedsize = true, penwidth = 2, fontsize = 8,
       fontname = "Arial", color = "#c5c8c6", fontcolor = "#c5c8c6"]
  Crawler
  Scraper

  node[shape = box, group = fbranch, color = "#b5bd68", fontcolor = "#b5bd68"]
  P1[label="Page 1"]
  P2[label="Page 2"]
  P3[label="Page ..."]
  P4[label="Page n"]

  node[shape = box, color = "#81a2be", fontcolor = "#81a2be"]
  L1[label="Ligne 1"]
  L2[label="Ligne 2"]
  L3[label="Ligne ..."]
  L4[label="Ligne n"]

  DATA[shape = none, color = "#cc6666", fontcolor = "#cc6666",
       label=<
  <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
  <TR><TD>Ligne 1</TD></TR>
  <TR><TD>Ligne 2</TD></TR>
  <TR><TD>Ligne ...</TD></TR>
  <TR><TD>Ligne n</TD></TR>
  </TABLE>
  >];

  Crawler->P1:w
  Crawler->P2:w
  Crawler->P3:w
  Crawler->P4:w
  P1:e->Scraper:nw
  P2:e->Scraper:w
  P3:e->Scraper:w
  P4:e->Scraper:sw
  Scraper->L1:w
  Scraper->L2:w
  Scraper->L3:w
  Scraper->L4:w
  L1:e->DATA:w
  L2:e->DATA:w
  L3:e->DATA:w
  L4:e->DATA:w
}
')

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 ?

Quelles connaissances sont nécessaires ?

Notre exemple : Allociné

Pourquoi Allociné ?

La page cible (scraper)

Page cible : les notes presse

L'index (crawler)

Page index

Et concrètement ?

Distribution des évaluations (étoiles)

ratings %>%
  ggplot(aes(x = rating)) +
  geom_bar() +
  labs(x = "Note (étoiles)", y = "",
       title = "Distribution des évaluations presse",
       caption = "Données : Allociné, films avec avis presse")

Au fil du temps

ratings %>%
  group_by(year) %>%
  summarise(mean_rating = mean(rating),
            sd_rating = sd(rating),
            min_sd = mean(rating) - sd(rating),
            max_sd = mean(rating) + sd(rating)) %>%
  ggplot(aes(x = year, y = mean_rating, fill = sd_rating)) +
  geom_crossbar(aes(ymin = min_sd, ymax = max_sd), colour = crossbar_col) +
  scale_fill_viridis() +
  labs(x = "", y = "Note (étoiles)",
       fill = "Écart-type",
       title = "Évolution des évaluations presse au fil des ans",
       caption = "Données : Allociné, films avec avis presse")

Selon la revue

ratings %>%
  keep_main_papers() %>%
  mutate(paper = factor(paper)) %>%
  group_by(paper) %>%
  summarise(nb_rating = n(),
            mean_rating = mean(rating),
            sd_rating = sd(rating),
            min_sd = mean(rating) - sd(rating),
            max_sd = mean(rating) + sd(rating)) %>%
  mutate(paper = glue("{paper} (n = {nb_rating})")) %>%
  ggplot(aes(x = fct_reorder(paper, mean_rating),
             y = mean_rating,
             fill = sd_rating)) +
  geom_crossbar(aes(ymin = min_sd, ymax = max_sd),
                colour = crossbar_col) +
  coord_flip() +
  scale_fill_viridis() +
  labs(x = "", y = "Note (étoiles)",
       fill = "Écart-type",
       title = "Distribution des évaluations selon la revue",
       caption = "Données : Allociné, films avec avis presse")

Dans le temps, par revue

ratings %>%
  keep_main_papers() %>%
  group_by(year, paper) %>%
  summarise(mean_rating = mean(rating),
            sd_rating = sd(rating),
            min_sd = mean(rating) - sd(rating),
            max_sd = mean(rating) + sd(rating)) %>%
  ggplot(aes(x = year, y = mean_rating, fill = sd_rating)) +
  geom_crossbar(aes(ymin = min_sd, ymax = max_sd),
                colour = crossbar_col) +
  facet_wrap(~ paper) +
  scale_fill_viridis() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(x = "", y = "Note (étoiles)",
       fill = "Écart-type",
       title = "Évolution des évaluations presse au fil des ans",
       subtitle = "Pour chaque revue",
       caption = "Données : Allociné, films avec avis presse")

De proche en proche

Le paquet Socscrap

Que contient Socscrap ?

Comment ça marche ?

DiagrammeR::grViz('
digraph {
  bgcolor = "#1d1f21";
  rankdir = LR;

  edge[color = "#c5c8c6"]

  node[shape = box, penwidth = 2,
       fontname = "Arial", color = "#c5c8c6", fontcolor = "#c5c8c6"]
  F1[label="get_ratings"]
  F2[label="process_filmlist"]
  F3[label="get_film_ratings"]
  F4[label="get_film_metadata"]
  F5[label="get_press_ratings"]
  F6[label="get_text"]

  F1->F2->F3;
  F3->F4;
  F3->F5;
  F5->F6;
}
', height = 200)

Testez le !

get_ratings(2017, pages = 4)

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()

Que fait le scraper ?

DiagrammeR::grViz('
digraph {
  bgcolor = "#1d1f21";
  rankdir = LR;

  edge[color = "#c5c8c6"]

  node[shape = box, penwidth = 2,
       fontname = "Arial", color = "#c5c8c6", fontcolor = "#c5c8c6"]
  F3[label="get_film_ratings"]
  F4[label="get_film_metadata"]
  F5[label="get_press_ratings"]

  node[shape = box, penwidth = 2,
       fontname = "Arial", color = "#b5bd68", fontcolor = "#b5bd68"]
  P4[label="Fiche du film"]
  P5[label="Notes presse\n du film"]

  node[shape = box, penwidth = 2,
       fontname = "Arial", color = "#f0c674", fontcolor = "#f0c674"]
  D4[label="Métadonnées"]
  D5[label="Notes presse"]

  node[shape = box, penwidth = 0,
       fontname = "Arial", color = "#cc6666", fontcolor = "#cc6666"]
  D0[label=<
  <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="5">
  <TR><TD>Notes</TD><TD>Métadonnées</TD></TR>
  </TABLE>
  >]

  F3->F4;
  F3->F5;
  F4->P4;
  F5->P5;
  P4->D4;
  P5->D5;
  D4->D0;
  D5->D0;
}
')

get_press_ratings

"https://www.allocine.fr/film/fichefilm-215099/critiques/presse/" %>%
  get_press_ratings()

get_film_metadata

"https://www.allocine.fr/film/fichefilm_gen_cfilm=215099.html" %>%
  get_film_metadata()

Page cible secondaire : fiche film

Fiche film

L'inspecteur

Que voit-on dans la console ?

Afficher une page web

Manipuler une page web

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)

DiagrammeR::grViz('
digraph {
  bgcolor = "#1d1f21";
  rankdir = LR;

  edge[color = "#c5c8c6"]

  node[shape = box, penwidth = 2, fixedsize = TRUE,
       fontname = "Arial", color = "#c5c8c6", fontcolor = "#c5c8c6"]
  div;p;ul;
  li1[label="li"]
  li2[label="li"]

  div->p;
  div->ul;
  ul->li1;
  ul->li2;
}
')

HTML : XPATH

//div/ul/li

DiagrammeR::grViz('
digraph {
  bgcolor = "#1d1f21";
  rankdir = LR;

  edge[color = "#c5c8c6"]

  node[shape = box, penwidth = 2, fixedsize = TRUE,
       fontname = "Arial", color = "#c5c8c6", fontcolor = "#c5c8c6"]
  div;p;ul;
  li1[label="li"]
  li2[label="li"]

  div->p;
  div->ul;
  ul->li1;
  ul->li2;
}
')

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

Solution

"https://www.allocine.fr/film/fichefilm_gen_cfilm=215099.html" %>%
  xml2::read_html() %>%
  get_text(".date")

Paquets utiles

Tidyverse

rnorm(100) %>%
  mean()

Here

read_csv(here::here("data", "enquete_emploi.csv"))

Glue

library(glue)

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

a <- 41

glue("La réponse à votre question est {a+1}.")

Rvest et xml2

Écrire des fonctions dans R

Faire ses propres fonctions, à quoi bon ?

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)

Écrire de meilleures fonctions

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)

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 ?

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)

Aux cartes !

library(purrr)

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

print(films)

Autres exemples

map_chr(films$real, ~ str_extract(., "\\d"))

map_int(films$real, ~ as.integer(str_extract(., "\\d")))

map2_chr(films$titre, films$real, ~ glue("{.x} par {.y}"))

Avantages de purrr

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())

print(resultat)

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)


gaalcaras/socscrap documentation built on Jan. 1, 2021, 2:16 a.m.