Aller au contenu
Retour au blog

Web scraping en R : guide pratique avec rvest et httr2

Raluca PenciucDernière mise à jour le 23 min read
Web scraping en R : guide pratique avec rvest et httr2
En bref : pour le web scraping en R, utilisez rvest pour analyser le HTML avec des sélecteurs CSS ou XPath, et httr2 pour récupérer les pages avec relances et limitation de débit. rvest seul suffit pour une page ; dès que vous parcourez un site, récupérez avec httr2 et analysez avec rvest, et passez à read_html_live() quand JavaScript construit la page. Ce guide couvre les tableaux, la pagination, les données dans les balises script, les API de scraping et les tests hors ligne.

Les données dont vous avez besoin se trouvent sur une page web, et vous voulez les récupérer dans un tibble sans quitter R. Le web scraping en R vous y mène avec deux packages et le tidyverse que vous connaissez déjà. Vous découvrez le sujet ? Commencez par ce qu'est le web scraping.

Ce guide prend Goodreads comme fil conducteur : la liste de lecteurs « Best Books Ever » et la page du 1984 de George Orwell. Vous allez analyser une page et un tableau avec rvest, parcourir poliment une liste paginée avec httr2, extraire des données construites par JavaScript, confier les pages bloquées à une API de scraping sans toucher à votre parseur, tester votre code hors ligne et résumer le résultat.

Démarrage rapide

  • Installation : install.packages(c("rvest", "httr2"))
  • Chargez avec read_html(), sélectionnez avec html_elements(), extrayez avec html_text2() ou html_attr()
  • Utilisez read_html_live() pour les pages construites par JavaScript
  • Utilisez R 4.1 ou une version ultérieure pour le pipe natif |>

De quoi avez-vous besoin pour faire du web scraping en R ?

Il vous faut R 4.1 ou une version ultérieure et deux packages : rvest pour analyser le HTML et httr2 pour récupérer les pages avec relances et limitation de débit. Ajoutez chromote quand les pages sont construites par JavaScript, et une API de scraping quand les sites vous bloquent. Tout le reste, de dplyr à readr, c'est le tidyverse que vous utilisez déjà.

R se prête bien au scraping, car les lignes extraites arrivent directement dans un tibble : le nettoyage et l'analyse se font dans la même session.

rvest analyse : il sélectionne des éléments avec des sélecteurs CSS ou XPath et en extrait le texte, les attributs et les tableaux. httr2 récupère : il définit les en-têtes, espace les requêtes, relance les échecs temporaires et exécute des requêtes en parallèle. rvest sait encore récupérer une URL tout seul, ce qui suffit pour une page. Dès que vous parcourez un site, récupérez avec httr2 et analysez avec rvest.

Package

Rôle

Quand l'utiliser

Statut CRAN (oct. 2026)

rvest

Analyse du HTML, sélection en CSS ou XPath, tableaux, read_html_live()

Toujours

1.0.5

xml2

Moteur HTML/XML sous rvest ; url_absolute()

Résolution des liens

1.6.0

httr2

Récupération avec en-têtes, relances, limitation de débit, requêtes parallèles

Plus d'une poignée de pages, ou des API

1.3.0

jsonlite

Analyse du JSON issu d'endpoints ou de balises script

Données livrées en JSON

2.0.0

chromote

Chrome headless (utilisé par read_html_live())

Pages construites par JavaScript

0.5.1

RSelenium

Selenium WebDriver depuis R

Installation Selenium existante ou besoin de Firefox

1.7.10 (nécessite Java et un driver)

testthat

Tests hors ligne du parseur

Tout scraper que vous relancerez

3.3.2

polite, Rcrawler

Anciens utilitaires de politesse et de crawl

Ne démarrez pas de nouveau code avec

Archivés sur CRAN en octobre 2026

Rcrawler et polite ont tous deux été archivés sur CRAN (Rcrawler en avril 2025, polite en septembre 2026). Les anciens tutoriels qui en dépendent ne s'installent donc plus proprement. Vous n'en avez pas besoin : req_throttle() de httr2 gère le rythme des requêtes, et la vérification du robots.txt présentée plus loin couvre le reste de ce que faisait polite.

Comment configurer R pour le scraping ?

Téléchargez R depuis le CRAN si vous ne l'avez pas. La version actuelle est R 4.6.2, et tous les extraits de ce guide fonctionnent avec R 4.1 ou une version ultérieure. N'importe quel éditeur convient : RStudio, Positron ou VS Code. Installez ensuite en une ligne les packages utilisés dans ce guide :

install.packages(c("rvest", "httr2", "jsonlite", "dplyr", "readr", "chromote", "testthat"))

read_html_live() a aussi besoin de Google Chrome sur votre machine. chromote le trouve automatiquement.

Créez un dossier de projet avec un sous-dossier fixtures/ pour les pages enregistrées. Enregistrez les trois pages Goodreads que ce guide analyse hors ligne :

download.file("https://www.goodreads.com/list/show/1.Best_Books_Ever", "fixtures/list-page1.html")
download.file("https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2", "fixtures/list-page2.html")
download.file("https://www.goodreads.com/book/show/5470.1984", "fixtures/book-1984.html")

Comment scraper une page web avec rvest ?

Chargez la page avec read_html(), sélectionnez les éléments répétés avec html_elements() et un sélecteur CSS, puis extrayez les champs de chacun avec html_element() et html_text2() ou html_attr(). Construisez un tibble à partir de ces vecteurs. Comme les fonctions de rvest sont vectorisées, une boucle est rarement utile, et un champ manquant renvoie NA au lieu de tout casser.

La liste Goodreads « Best Books Ever » affiche 100 livres par page. Chaque livre occupe une ligne d'un tableau de classe tableList, et chaque ligne contient un rang, un lien vers le titre, un lien vers l'auteur et une ligne du type « 4.36 avg rating — 10,338,532 ratings ». Cette fonction transforme une page de la liste en tibble :

# parse_books.R: turn one Goodreads list page into a tibble
library(rvest)
library(tibble)

parse_books <- function(page, page_url) {
  rows  <- html_elements(page, "table.tableList tr")
  links <- html_element(rows, "a.bookTitle")
  stats <- html_text2(html_element(rows, "span.minirating"))   # "4.36 avg rating — 10,338,532 ratings"
  tibble(
    rank       = as.integer(html_text2(html_element(rows, "td.number"))),
    title      = html_text2(links),
    author     = html_text2(html_element(rows, "a.authorName")),
    avg_rating = readr::parse_number(stats),
    ratings    = readr::parse_number(sub(".*avg rating", "", stats)),
    url        = xml2::url_absolute(html_attr(links, "href"), page_url)
  )
}

Ligne par ligne :

  • html_elements(page, "table.tableList tr") renvoie toutes les lignes : un nœud par livre.
  • html_element(rows, "a.bookTitle"), au singulier, renvoie exactement un résultat par ligne. C'est ce qui garde les colonnes alignées. S'il manquait un auteur sur une ligne, vous obtiendriez NA à cet endroit, et non un vecteur plus court qui décalerait toutes les valeurs suivantes.
  • html_text2() renvoie le texte tel qu'un navigateur l'affiche, en fusionnant les retours à la ligne et les suites d'espaces. L'ancienne html_text() renvoie le texte brut, indentation comprise.
  • readr::parse_number() lit le premier nombre d'une chaîne : la ligne de notation devient donc 4.36. En supprimant d'abord tout ce qui précède « avg rating », il reste « — 10,338,532 ratings », qui donne 10338532. Les deux colonnes sont des nombres que vous pouvez trier et moyenner, pas des chaînes.
  • xml2::url_absolute() transforme chaque href relatif, comme /book/show/2767052-the-hunger-games, en URL complète. Résolvez les liens par rapport à la page d'où ils viennent, pas par rapport à une racine de site saisie à la main. Un lien peut être relatif au dossier de la page, et une mauvaise base produit des URL qui renvoient une 404. La référence de url_absolute() détaille les règles.

Lancez-la sur la page enregistrée :

# 01_static.R: parse a saved page (offline); the same code works on a live URL
source("parse_books.R")
page  <- read_html("fixtures/list-page1.html")
books <- parse_books(page, "https://www.goodreads.com/list/show/1.Best_Books_Ever")
print(books, n = 5, width = 100)
cat("rows:", nrow(books), "\n")

Sortie :

# A tibble: 100 × 6
   rank title                                                       
  <int> <chr>                                                       
1     1 The Hunger Games (The Hunger Games, #1)                     
2     2 Pride and Prejudice                                         
3     3 To Kill a Mockingbird                                       
4     4 Harry Potter and the Order of the Phoenix (Harry Potter, #5)
5     5 The Book Thief                                              
  author          avg_rating  ratings url                                       
  <chr>                <dbl>    <dbl> <chr>                                     
1 Suzanne Collins       4.36 10338532 https://www.goodreads.com/book/show/27670…
2 Jane Austen           4.3   5042082 https://www.goodreads.com/book/show/1885.…
3 Harper Lee            4.26  7157438 https://www.goodreads.com/book/show/2657.…
4 J.K. Rowling          4.5   3954056 https://www.goodreads.com/book/show/2.Har…
5 Markus Zusak          4.39  2994493 https://www.goodreads.com/book/show/19063…
# ℹ 95 more rows
rows: 100

Une copie enregistrée vous permet d'itérer sans envoyer de requête à chaque exécution. read_html() accepte aussi une URL : read_html("https://www.goodreads.com/list/show/1.Best_Books_Ever") analyse la liste en ligne.

Comment trouver les bons sélecteurs CSS ?

Ouvrez la page dans Chrome ou Firefox, faites un clic droit sur les données voulues et choisissez Inspecter. Repérez une classe ou une balise qui se répète une fois par enregistrement, testez-la dans la console des DevTools avec document.querySelectorAll(), puis collez le même sélecteur dans html_elements(). Préférez des noms de classe stables aux longs chemins générés copiés depuis les DevTools.

Sur la liste Goodreads, inspecter un titre de livre montre un <a class="bookTitle"> à l'intérieur d'un <tr> de table.tableList. Taper document.querySelectorAll("table.tableList tr").length dans la console renvoie 100, un par livre, ce qui confirme qu'il s'agit du bon élément répété.

Six motifs de sélecteurs apparaissent dans ce guide, et ils couvrent l'essentiel du travail de scraping :

  • Balise.classe : a.bookTitle cible les éléments <a> de classe bookTitle.
  • Descendant : table.tableList tr cible les lignes situées n'importe où dans ce tableau.
  • Classe seule : .minirating cible tout élément portant cette classe (nous l'écrivons span.minirating pour être explicites).
  • ID : script#__NEXT_DATA__ cible l'unique élément portant cet id.
  • Valeur d'attribut : script[type="application/ld+json"] cible les scripts d'un type donné.
  • Attribut d'un contrôle : button[aria-label="Book details and editions"] trouve un bouton par son nom accessible, qui survit généralement aux changements de style.

Évitez les longs chemins que produit « Copy selector » dans les DevTools, comme #all_votes > table > tbody > tr:nth-child(1) > td:nth-child(3). Ils cassent dès que la mise en page bouge. Les noms de classe générés avec des suffixes aléatoires sont fragiles pour la même raison.

Si vous préférez pointer et cliquer, l'extension de navigateur SelectorGadget propose un sélecteur quand vous cliquez sur les éléments voulus. Vérifiez d'abord sa suggestion dans la console.

rvest accepte aussi XPath : html_elements(page, xpath = "//a[@class='bookTitle']") fait le même travail que la version CSS. Lisez quand XPath l'emporte sur CSS pour savoir dans quels cas c'est le meilleur outil, et gardez la référence complète des sélecteurs ouverte pendant que vous travaillez.

Astuce : si un sélecteur fonctionne dans les DevTools mais ne renvoie aucun élément dans R, les données ne figurent peut-être pas dans le HTML envoyé par le serveur. Les DevTools montrent la page après l'exécution de JavaScript, read_html() la voit avant.

Comment scraper un tableau HTML en R ?

Sélectionnez le tableau avec html_element() et passez-le à html_table(), qui renvoie un tibble avec une colonne par colonne du tableau. Indiquez header = FALSE quand le tableau n'a pas de ligne d'en-tête, puis renommez les colonnes. Nettoyez ensuite les nombres avec readr::parse_number(), car html_table() conserve les symboles monétaires et le texte tels qu'affichés.

La liste Goodreads est un vrai tableau HTML, donc html_table() la lit en un seul appel :

# 03_table.R: read the same list with html_table()
library(rvest)
page <- read_html("fixtures/list-page1.html")
raw  <- page |> html_element("table.tableList") |> html_table(header = FALSE)
dim(raw)
raw$X3 <- trimws(gsub("\\s+", " ", raw$X3))   # collapse the cell's line breaks
print(raw[, c("X1", "X3")], n = 3, width = 100)

Sortie :

[1] 100   4
# A tibble: 100 × 2
     X1
  <int>
1     1
2     2
3     3
  X3                                                                            
  <chr>                                                                         
1 The Hunger Games (The Hunger Games, #1) by Suzanne Collins 4.36 avg rating — …
2 Pride and Prejudice by Jane Austen 4.30 avg rating — 5,042,082 ratings score:…
3 To Kill a Mockingbird by Harper Lee 4.26 avg rating — 7,157,438 ratings score…

Vous obtenez 100 lignes et quatre colonnes. La première colonne est le rang, déjà typé en entier. La troisième montre la limite : Goodreads regroupe le titre, l'auteur, la note et le score des votes dans une seule cellule, et html_table() vous rend donc le tout en une seule chaîne.

html_table() est l'option la plus rapide quand chaque cellule contient une seule valeur, comme dans une grille tarifaire ou un tableau de statistiques. Quand une cellule regroupe plusieurs champs, sélectionnez plutôt à l'intérieur de chaque ligne, comme le fait parse_books(), pour obtenir des colonnes propres et typées.

Appelé sur une page entière, html_table() renvoie une liste de tous les tableaux. Sélectionnez donc d'abord celui qui vous intéresse.

Comment scraper plusieurs pages en R ?

Scrapez la première page, lisez le lien vers la page suivante, résolvez-le avec xml2::url_absolute(), et recommencez jusqu'à ce que le lien disparaisse ou que vous atteigniez une limite de pages. Récupérez via httr2, avec req_throttle() pour plafonner votre débit de requêtes et req_retry() pour relancer les réponses 429 et 503, en respectant aussi l'en-tête Retry-After.

read_html(url) convient pour une page. Pour un crawl, récupérez plutôt avec httr2, car vous contrôlez le user agent, le délai d'expiration, le débit et les relances dans un seul pipeline :

# 02_paginate.R: polite live crawl with httr2 retries, throttling, and next-page links
library(httr2)
source("parse_books.R")

fetch_html <- function(url) {
  request(url) |>
    req_user_agent("r-scraping-tutorial/1.0 (you@example.com)") |>
    req_throttle(capacity = 1, fill_time_s = 2) |>   # at most ~1 request every 2 seconds
    req_retry(max_tries = 3) |>                      # retries 429 and 503, honors Retry-After
    req_timeout(30) |>
    req_perform() |>
    resp_body_html()
}

scrape_list <- function(start_url, max_pages = 3) {
  url <- start_url
  out <- list()
  for (i in seq_len(max_pages)) {
    page     <- fetch_html(url)
    out[[i]] <- parse_books(page, url)
    next_href <- html_attr(html_element(page, "a.next_page"), "href")
    if (is.na(next_href)) break
    url <- xml2::url_absolute(next_href, url)
  }
  dplyr::bind_rows(out, .id = "page")
}

books <- scrape_list("https://www.goodreads.com/list/show/1.Best_Books_Ever", max_pages = 3)
print(dplyr::count(books, page))
print(range(books$rank))

Sortie :

# A tibble: 3 × 2
  page      n
  <chr> <int>
1 1       100
2 2       100
3 3       100
[1]   1 300

Ce que fait chaque partie de fetch_html() :

  • req_user_agent() identifie votre scraper et donne au site un moyen de vous contacter. Indiquez-y une vraie adresse.
  • req_throttle(capacity = 1, fill_time_s = 2) est un seau à jetons qui contient une requête et se recharge toutes les deux secondes : le crawl envoie donc au plus environ une requête toutes les deux secondes à un même hôte, quelle que soit la vitesse de la boucle.
  • req_retry() relance les échecs temporaires. Par défaut, il s'agit de HTTP 429 Too Many Requests et de 503 Service Unavailable, et quand le serveur envoie un en-tête Retry-After, httr2 attend ce délai avant de réessayer.
  • req_timeout(30) empêche une requête bloquée de paralyser tout le crawl.

scrape_list() suit le lien « Next » (a.next_page) au lieu de deviner les numéros de page, le résout par rapport à l'URL courante, et s'arrête quand le lien disparaît ou qu'il atteint max_pages. bind_rows(.id = "page") enregistre la page d'origine de chaque ligne, et la plage de rank de 1 à 300 confirme qu'aucune page n'a été sautée ni répétée.

Gardez max_pages bas pendant le développement. Trois pages vous en apprennent autant que cent.

Comment scraper des pages en parallèle ?

req_perform_parallel() de httr2 prend une liste de requêtes et en exécute plusieurs à la fois, et max_active plafonne le nombre de requêtes en cours (10 par défaut). Construisez la liste avec lapply() sur vos URL, et ajoutez req_throttle() à chaque requête pour que le débit cumulé vers un même hôte reste raisonnable. Ne lancez pas un worker par cœur de processeur contre le même site : cela multiplie votre trafic et vous fait bloquer rapidement. Pour des milliers d'URL, le goulot d'étranglement est le réseau, pas R : analyser une page prend quelques millisecondes. Répartissez le travail parallèle entre plusieurs hôtes, pas sur un seul.

Comment savoir si une page a besoin de JavaScript ?

Récupérez le HTML brut avec read_html() et vérifiez si votre sélecteur trouve quelque chose. S'il ne renvoie aucun élément alors que le navigateur affiche les données, c'est JavaScript qui construit ce contenu. Avant de recourir à un navigateur, regardez dans les balises script : beaucoup de sites livrent les enregistrements en JSON, que jsonlite peut analyser directement.

La page de 1984 sur Goodreads affiche un bouton « Book details and editions ». Cliquez dessus dans un navigateur et un panneau s'ouvre avec le format, l'éditeur et la langue de l'édition. Dans le HTML brut, ce panneau n'existe pas encore. Mais les données qui l'alimentent, si, dans deux balises script :

# 04_js_check.R: is the data in the HTML, or built by JavaScript?
library(rvest)
page <- read_html("fixtures/book-1984.html")
cat("edition details in raw HTML:", length(html_elements(page, "div.EditionDetails")), "\n")

# 1. JSON-LD: the structured summary many sites publish for search engines
ld <- page |> html_element('script[type="application/ld+json"]') |> html_text2() |> jsonlite::fromJSON()
book <- tibble::tibble(
  title      = ld$name,
  author     = ld$author$name[1],
  pages      = ld$numberOfPages,
  avg_rating = ld$aggregateRating$ratingValue,
  ratings    = ld$aggregateRating$ratingCount
)
print(book)

# 2. __NEXT_DATA__: the app state the page's JavaScript renders from
next_data <- page |> html_element("script#__NEXT_DATA__") |> html_text2() |>
  jsonlite::fromJSON(simplifyVector = FALSE)
state     <- next_data$props$pageProps$apolloState
this_book <- Filter(\(x) identical(x$`__typename`, "Book") && identical(x$legacyId, 5470L), state)[[1]]
cat("publisher:", this_book$details$publisher, "| language:", this_book$details$language$name, "\n")

Sortie :

edition details in raw HTML: 0 
# A tibble: 1 × 5
  title author        pages avg_rating ratings
  <chr> <chr>         <int>      <dbl>   <int>
1 1984  George Orwell   328       4.21 5812589
publisher: New American Library | language: English 

Le sélecteur ne trouve rien : c'est donc JavaScript qui construit le panneau. Pourtant, la page contient bien les données.

JSON-LD (script[type="application/ld+json"]) est un résumé structuré que beaucoup de sites publient pour les moteurs de recherche. Il est compact, documenté par schema.org et change moins souvent que la mise en page : cherchez-le en premier sur les pages de produits, de livres, de recettes et d'événements.

__NEXT_DATA__ est l'état à partir duquel le framework JavaScript de la page fait son rendu. Goodreads stocke les enregistrements dans une liste plate indexée par type. Le code garde donc l'entrée Book dont l'ID correspond à ce livre. Elle contient des champs absents du HTML visible, comme l'éditeur, mais sa structure est interne au site et peut changer sans préavis : couvrez-la par des tests.

Troisième source, les endpoints JSON du site lui-même. Dans les DevTools, ouvrez Network, filtrez sur Fetch/XHR et rechargez. httr2 peut appeler un endpoint repéré à cet endroit, et resp_body_json() analyse la réponse, à condition que le robots.txt autorise son chemin.

Ces trois sources sont généralement plus rapides et plus stables que le rendu de la page dans un navigateur.

Comment scraper des pages rendues par JavaScript en R ?

Utilisez read_html_live() de rvest, qui charge la page dans un Chrome headless via chromote et vous permet d'appliquer les fonctions rvest habituelles au DOM rendu. Elle sait aussi cliquer, saisir du texte et faire défiler la page. Installez d'abord Google Chrome. Pour les gros volumes ou les sites qui bloquent, faites le rendu à distance avec une API de scraping plutôt que d'exécuter Chrome vous-même.

read_html_live() ouvre la page de 1984, clique sur le bouton des détails et lit le panneau qui apparaît :

# 05_live.R: render the page in headless Chrome and click a button (rvest >= 1.0.4, chromote + Chrome)
library(rvest)
options(chromote.timeout = 30)   # heavy pages can take longer than the 10-second default
page <- read_html_live("https://www.goodreads.com/book/show/5470.1984")
Sys.sleep(2)   # simple wait; see the note on waiting for a selector
page$click('button[aria-label="Book details and editions"]')
Sys.sleep(1)
items <- page$html_elements("div.EditionDetails div.DescListItem")
details <- tibble::tibble(
  field = items |> html_element("dt") |> html_text2(),
  value = items |> html_element("dd") |> html_text2()
)
print(details)
invisible(page$session$close())

Sortie :

# A tibble: 3 × 2
  field     value                               
  <chr>     <chr>                               
1 Format    328 pages, Mass Market Paperback    
2 Published July 1, 1950 by New American Library
3 Language  English                             

read_html_live() est arrivée avec rvest 1.0.4 et renvoie un objet de page en direct. Sa méthode $html_elements() fonctionne comme la fonction que vous connaissez déjà, et le résultat s'enchaîne directement avec html_element() et html_text2(). Le même objet peut faire $click() sur un sélecteur, $type() dans un champ et $scroll_to() vers une position, ce qui couvre les boutons « charger plus » et le défilement infini. La référence de read_html_live() liste toutes les méthodes.

Sys.sleep() est l'attente la plus simple qui soit. Sur les pages lentes, utilisez une petite boucle qui vérifie length(page$html_elements("div.EditionDetails")) toutes les demi-secondes, jusqu'à ce que la valeur dépasse zéro ou qu'un délai maximal soit écoulé. options(chromote.timeout = 30) laisse plus de temps aux pages lourdes pour se charger. Fermez la session quand vous avez terminé, car chacune maintient un processus Chrome actif.

Vous découvrez les navigateurs sans fenêtre ? Lisez comment fonctionnent les navigateurs headless.

RSelenium est la voie plus ancienne. Il est toujours sur le CRAN et utile si votre équipe fait déjà tourner une Selenium Grid ou a besoin de Firefox. Le prix à payer, c'est l'installation : Java, un serveur ou un driver Selenium, et des versions de navigateur et de driver qui doivent correspondre.

Un Chrome local convient bien pour une poignée de pages. Quand vous avez besoin de sessions de navigateur gérées dans le cloud, avec attentes et actions, utilisez plutôt un navigateur automatisé hébergé.

Comment scraper depuis R des pages bloquées avec une API de scraping ?

Envoyez l'URL cible à une API de scraping depuis httr2 et analysez la réponse avec rvest comme d'habitude. Avec le Scraper API de WebScrapingAPI, vous appelez un seul endpoint avec votre api_key et l'URL. Ajoutez render_js=1 pour les pages JavaScript. Gardez la clé dans .Renviron, jamais dans le code. Vos fonctions d'analyse ne changent pas.

Les relances et la limitation de débit règlent la plupart des problèmes sur les sites accommodants. Les symptômes suivants indiquent que la récupération locale est le goulot d'étranglement :

  • des réponses 403 ou 429 qui reviennent malgré des relances raisonnables
  • des pages de vérification ou de consentement à la place du contenu
  • un contenu qui varie selon le pays, alors qu'il vous faut la version d'un pays précis
  • plus de pages JavaScript à rendre que votre Chrome local ne peut en traiter

Avant de changer votre couche de récupération, essayez les bases. Il existe d'autres façons de limiter les blocages, et quatre comptent plus que les autres :

  • Ralentissez : augmentez fill_time_s dans req_throttle().
  • Envoyez un user agent honnête avec vos coordonnées.
  • Mettez en cache les pages déjà récupérées au lieu de les redemander.
  • Ne scrapez pas ce dont vous n'avez pas besoin, comme les images ou les pages hors de votre question.

Si les symptômes persistent, changez de couche de récupération. Le Scraper API de WebScrapingAPI prend une URL et renvoie la page, en gérant de son côté les proxys, les relances et, en option, le rendu JavaScript. Il n'existe pas de SDK R, et vous n'en avez pas besoin : une seule fonction httr2 constitue toute l'intégration.

# 06_scraper_api.R: call WebScrapingAPI's Scraper API from R with httr2
library(httr2)
library(rvest)

wsa_get <- function(url, render_js = FALSE, country = NULL) {
  key <- Sys.getenv("WSA_API_KEY")
  if (key == "") stop("Set WSA_API_KEY in your environment (for example in ~/.Renviron).")
  request("https://api.webscrapingapi.com/v2") |>
    req_url_query(
      api_key   = key,
      url       = url,                       # httr2 URL-encodes query values
      render_js = if (render_js) 1 else NULL,
      country   = country
    ) |>
    req_retry(max_tries = 3) |>
    req_timeout(120) |>
    req_perform()
}

# See exactly what you'll send, without sending it
req <- request("https://api.webscrapingapi.com/v2") |>
  req_url_query(api_key = "YOUR_KEY",
                url = "https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2",
                country = "us")
req$url

Sortie :

[1] "https://api.webscrapingapi.com/v2?api_key=YOUR_KEY&url=https%3A%2F%2Fwww.goodreads.com%2Flist%2Fshow%2F1.Best_Books_Ever%3Fpage%3D2&country=us"

L'URL affichée montre pourquoi req_url_query() compte. L'URL cible a son propre ?page=2. Collés tels quels dans une autre URL, ce ? et tout & seraient lus comme des paramètres de l'API, qui récupérerait alors la mauvaise page. httr2 les encode en %3F et %26, et la cible arrive intacte. Les arguments NULL sont ignorés : render_js et country n'apparaissent que si vous les définissez.

Gardez la clé hors de votre code. Ajoutez cette ligne au fichier .Renviron de votre répertoire personnel, puis redémarrez R :

WSA_API_KEY=your_api_key_here

Sys.getenv("WSA_API_KEY") la lit, et le manuel de R explique comment R lit .Renviron au démarrage. Si la variable n'est pas définie, wsa_get() s'arrête avec Error: Set WSA_API_KEY in your environment (for example in ~/.Renviron). Une clé invalide renvoie un HTTP 401 avec le corps {"error":"Invalid API key"}, que httr2 transforme en erreur R : une mauvaise clé ne passe donc jamais inaperçue sous la forme d'une page vide.

render_js = TRUE exécute le JavaScript de la page avant de renvoyer le HTML. country = "us" récupère la page telle qu'un visiteur de ce pays la verrait. Le rendu JavaScript et le géociblage ne coûtent aucun crédit supplémentaire.

La réponse passe dans la même fonction parse_books() que vous avez écrite au début :

source("parse_books.R")
source("wsa_get.R")   # the wsa_get() function from the previous snippet

target <- "https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2"
page   <- wsa_get(target, country = "us") |> httr2::resp_body_html()
books  <- parse_books(page, target)
print(books, n = 5, width = 100)
cat("rows:", nrow(books), "\n")

# Ask the API to run the page's JavaScript before it returns the HTML
book_page <- wsa_get("https://www.goodreads.com/book/show/5470.1984", render_js = TRUE) |>
  httr2::resp_body_html()
html_text2(html_element(book_page, 'h1[data-testid="bookTitle"]'))

Sortie :

# A tibble: 100 × 6
   rank title                                                       
  <int> <chr>                                                       
1   101 The Alchemist                                               
2   102 Divergent (Divergent, #1)                                   
3   103 Fahrenheit 451                                              
4   104 The Catcher in the Rye                                      
5   105 The Handmaid's Tale                                         
  author           avg_rating ratings url                                       
  <chr>                 <dbl>   <dbl> <chr>                                     
1 Paulo Coelho           3.91 3382145 https://www.goodreads.com/book/show/18144…
2 Veronica Roth          4.13 3905122 https://www.goodreads.com/book/show/13335…
3 Ray Bradbury           3.97 2488011 https://www.goodreads.com/book/show/13079…
4 J.D. Salinger          3.8  3600144 https://www.goodreads.com/book/show/5107.…
5 Margaret Atwood        4.13 2145887 https://www.goodreads.com/book/show/38447…
# ℹ 95 more rows
rows: 100
[1] "1984"

Seule la ligne de récupération a changé, pas le parseur. Chaque test que vous écrivez pour parse_books() couvre donc aussi ce chemin.

Quelle méthode de scraping en R choisir ?

Utilisez rvest seul pour les pages statiques et les sites d'entraînement. Ajoutez httr2 quand vous parcourez de nombreuses pages et avez besoin de limiter le débit et de relancer. Analysez le JSON des balises script ou l'endpoint JSON du site quand JavaScript charge les données. Utilisez read_html_live() pour quelques pages interactives, et une API de scraping quand les blocages ou le volume rendent la récupération locale peu fiable.

Situation

Outil

Pourquoi

HTML statique, quelques pages

read_html() + rvest

Le moins de pièces mobiles

Nombreuses pages ou crawl

Récupération httr2 (req_throttle(), req_retry()) + rvest

Limitation de débit, relances, délais d'expiration

Données en JSON dans <script> ou endpoint JSON

httr2 + jsonlite

Plus rapide et plus stable que le rendu

Page construite par JavaScript, faible volume

read_html_live()

Vrai Chrome, mêmes fonctions rvest

Clics, connexions, parcours en plusieurs étapes que vous avez le droit d'automatiser

Méthodes de read_html_live(), ou RSelenium

Prise en charge des interactions

403/429 que les relances ne règlent pas, contenu propre à un pays ou rendu à grande échelle

Scraper API depuis httr2

Proxys, relances et rendu gérés ; parseur inchangé

En cas de doute, parcourez cette liste dans l'ordre et arrêtez-vous au premier oui :

  1. Les données sont-elles dans le HTML brut ? Utilisez rvest.
  2. Vous faut-il plus que quelques pages ? Récupérez avec httr2 et analysez avec rvest.
  3. Les données sont-elles dans une balise script ou un endpoint JSON ? Analysez-les avec jsonlite.
  4. La page a-t-elle besoin d'un navigateur, et n'avez-vous que quelques pages ? Utilisez read_html_live().
  5. Êtes-vous bloqué, vous faut-il un pays précis, ou faites-vous du rendu à grande échelle ? Utilisez une API de scraping.

Comment tester un scraper et transformer les résultats en jeu de données ?

Enregistrez une copie de chaque type de page dans un fichier HTML et écrivez des tests testthat qui l'analysent hors ligne : une refonte du site fait alors échouer un test au lieu de renvoyer silencieusement des NA. Traitez ensuite le tibble extrait comme n'importe quelles données : résumez-le avec dplyr, écrivez-le avec readr::write_csv() et notez la date de collecte.

Les scrapers plantent rarement quand un site change. Ils renvoient des colonnes vides. Les tests sur des pages enregistrées le détectent. Vous avez déjà enregistré les fixtures avec download.file(). Si vous récupérez avec httr2, writeLines(resp_body_string(resp), "fixtures/page.html") fait la même chose. Versionnez-les dans le projet pour que les tests s'exécutent de la même façon sur toutes les machines.

Ce fichier testthat vérifie ce qui compte dans le parseur de la liste :

# tests/test-parse.R: offline tests against saved HTML, run with testthat::test_file()
library(testthat)
source("../parse_books.R")

test_that("list page parses into 100 clean rows", {
  books <- parse_books(read_html("../fixtures/list-page1.html"),
                       "https://www.goodreads.com/list/show/1.Best_Books_Ever")
  expect_equal(nrow(books), 100)
  expect_false(anyNA(books$avg_rating))
  expect_true(all(books$avg_rating >= 1 & books$avg_rating <= 5))
  expect_true(all(books$ratings > 0))
  expect_match(books$url, "^https://www\\.goodreads\\.com/book/show/")
})

Lancez-le depuis le dossier tests/ avec testthat::test_file("test-parse.R") :

[ FAIL 0 | WARN 0 | SKIP 0 | PASS 5 ]

Quand le site change, enregistrez une nouvelle copie de la page et relancez le test. Un échec vous indique quel champ a cassé avant que des données erronées n'atteignent votre analyse. Appliquez la même règle « échouer bruyamment » en production : si un sélecteur ne renvoie aucune ligne, arrêtez l'exécution avec une erreur au lieu d'écrire un fichier vide.

Les livres qui ont le plus de notes sont-ils aussi les mieux notés ? Deux pages enregistrées vous donnent 200 livres à examiner :

# 07_analysis.R: from scraped rows to an answer
library(dplyr, warn.conflicts = FALSE)
source("parse_books.R")
list_url <- "https://www.goodreads.com/list/show/1.Best_Books_Ever"
books <- bind_rows(
  parse_books(read_html("fixtures/list-page1.html"), list_url),
  parse_books(read_html("fixtures/list-page2.html"), paste0(list_url, "?page=2"))
)
books |>
  mutate(popularity = cut(ratings, c(0, 1e6, 3e6, Inf),
                          labels = c("under 1M", "1M to 3M", "over 3M"))) |>
  group_by(popularity) |>
  summarize(books = n(), median_rating = median(avg_rating), .groups = "drop") |>
  print()
books |> count(author, sort = TRUE) |> head(3) |> print()
readr::write_csv(books, "books.csv")
cat("wrote books.csv with", nrow(books), "rows\n")

Sortie :

# A tibble: 3 × 3
  popularity books median_rating
  <fct>      <int>         <dbl>
1 under 1M      84          4.1 
2 1M to 3M      79          4.14
3 over 3M       37          4.3 
# A tibble: 3 × 2
  author           n
  <chr>        <int>
1 J.K. Rowling    10
2 Rick Riordan     5
3 Riley Sager      4
wrote books.csv with 200 rows

Dans cet échantillon, les 37 livres qui dépassent trois millions de notes ont une note médiane de 4,3, contre 4,1 pour les livres qui en ont moins d'un million. C'est une tendance observée sur une liste votée par des lecteurs, pas une loi de l'édition. À partir de là, ce sont des données R ordinaires : un nuage de points ggplot2 de ratings contre avg_rating sur une échelle logarithmique est le graphique qui s'impose ensuite.

Comme la liste change chaque jour, ajoutez une colonne scraped_at = Sys.time() avant d'enregistrer. Quand quelqu'un relancera l'analyse le mois prochain et obtiendra d'autres chiffres, l'horodatage expliquera pourquoi.

R ou Python : lequel est le meilleur pour le web scraping ?

Python dispose de davantage de bibliothèques de scraping et d'outils de navigateur, ce qui le rend adapté aux gros crawlers et aux services de production. R convient mieux quand les données extraites alimentent des analyses, des rapports ou des modèles en R, car rvest renvoie des tibbles et httr2 assure une récupération respectueuse. Pour la plupart des analystes, rester dans un seul langage vaut mieux que d'en changer pour un scraping à peine plus rapide. Si les données finissent dans une analyse R, un rapport Quarto ou un modèle, scraper en R supprime un passage de relais : pas d'export, et pas de second environnement à synchroniser.

La couche de récupération est indépendante du langage, ce qui facilite les combinaisons. Un client HTTP ou une API de scraping renvoie le même HTML à R comme à Python : une équipe peut donc crawler dans un langage et analyser dans l'autre. Côté Python, consultez notre guide du web scraping avec Python.

Comment scraper de manière responsable en R ?

Lisez les conditions d'utilisation et le robots.txt du site avant de crawler, gardez un débit de requêtes faible, identifiez votre scraper avec un user agent et ne collectez que les champs dont vous avez besoin. Évitez les données personnelles sans base légale, et ne scrapez jamais derrière une connexion sans autorisation. En cas de doute, demandez au propriétaire du site ou utilisez son API.

Le robots.txt est le moyen par lequel un site indique aux robots les chemins qu'il ne veut pas voir visités. La norme robots.txt (RFC 9309) en définit les règles : trouvez le groupe correspondant à votre user agent, et la règle correspondante la plus spécifique l'emporte. Cette fonction les applique à une liste de chemins :

# 08_robots.R: check paths against a site's robots.txt before you crawl (RFC 9309)
library(httr2)

robots_allows <- function(site, paths, agent = "*") {
  resp <- request(site) |> req_url_path("/robots.txt") |>
    req_error(is_error = \(resp) FALSE) |> req_perform()
  if (resp_status(resp) == 404) return(rep(TRUE, length(paths)))   # no robots.txt, no rules
  lines <- trimws(sub("#.*", "", strsplit(resp_body_string(resp), "\n")[[1]]))
  lines <- lines[lines != ""]
  field <- tolower(trimws(sub(":.*", "", lines)))
  value <- trimws(sub("^[^:]*:", "", lines))

  # Keep the Allow/Disallow rules from groups whose User-agent line matches `agent`
  keep <- logical(length(lines)); in_group <- FALSE
  for (i in seq_along(lines)) {
    if (field[i] == "user-agent") {
      if (i == 1 || field[i - 1] != "user-agent") in_group <- FALSE
      in_group <- in_group || value[i] == agent
    } else {
      keep[i] <- in_group && field[i] %in% c("allow", "disallow") && value[i] != ""
    }
  }
  type <- field[keep]; rule <- value[keep]
  pattern <- paste0("^", gsub("*", ".*", gsub("([.?+^$()|{}\\[\\]\\\\])", "\\\\\\1", rule, perl = TRUE), fixed = TRUE))

  vapply(paths, function(path) {
    hits <- which(vapply(pattern, grepl, logical(1), x = path))
    if (length(hits) == 0) return(TRUE)
    best <- hits[nchar(rule[hits]) == max(nchar(rule[hits]))]   # longest match wins
    any(type[best] == "allow")                                  # a tie goes to Allow
  }, logical(1))
}

paths <- c("/list/show/1.Best_Books_Ever", "/book/show/5470.1984", "/book/reviews/5470",
           "/review/list/1", "/search?q=orwell", "/work/quotes/153313")
data.frame(path = paths, allowed = unname(robots_allows("https://www.goodreads.com", paths)))

Sortie :

                          path allowed
1 /list/show/1.Best_Books_Ever    TRUE
2         /book/show/5470.1984    TRUE
3           /book/reviews/5470   FALSE
4               /review/list/1   FALSE
5             /search?q=orwell   FALSE
6          /work/quotes/153313    TRUE

Les pages de liste et de livre utilisées dans ce guide sont autorisées. Les listes d'avis et la recherche ne le sont pas, c'est pourquoi ce guide travaille avec les notes au niveau du livre et ne collecte pas les avis individuels. La dernière ligne montre la règle de la correspondance la plus longue à l'œuvre : Goodreads interdit /work mais autorise /work/quotes, plus spécifique.

Le robots.txt est une convention entre robots d'exploration, pas une autorisation juridique, et ce n'est pas le seul document qui compte. Les conditions d'utilisation d'un site peuvent restreindre la collecte automatisée même sur des chemins que le robots.txt autorise. Les conditions de Goodreads, par exemple, excluent l'exploration de données (data mining) et les outils d'extraction automatisée de la licence qu'elles accordent. Lisez-les avant d'exécuter ces exemples, et demandez l'autorisation de Goodreads avant de collecter ses données pour tout usage qui dépasse l'apprentissage.

Les avis montrent aussi pourquoi les données personnelles demandent de la prudence : chacun porte le nom et les opinions d'un lecteur. Pour d'autres bonnes habitudes, consultez ces bonnes pratiques de scraping et nos principes pour des données web responsables.

FAQ

rvest est-il toujours maintenu ?

Oui. rvest fait partie du tidyverse, et la version 1.0.5 est disponible sur le CRAN en octobre 2026. La version 1.0.4 a ajouté read_html_live() pour les pages JavaScript. Pour la récupération, la même équipe maintient désormais httr2, qui gère les relances, la limitation de débit et les requêtes parallèles : la plupart des scrapers R modernes associent donc les deux packages.

Quelle est la différence entre rvest et httr2 ?

rvest analyse le HTML : il sélectionne des éléments en CSS ou XPath et extrait le texte, les attributs et les tableaux. httr2 envoie des requêtes HTTP : il définit les en-têtes, relance les échecs, limite votre débit et gère les API JSON. Vous pouvez scraper des pages simples avec rvest seul, mais pour un crawl, récupérez avec httr2 et analysez avec rvest.

rvest peut-il scraper des sites JavaScript ?

read_html() seule ne le peut pas, car elle ne voit que le HTML envoyé par le serveur. read_html_live(), si : elle exécute la page dans un Chrome headless via chromote et vous donne le DOM rendu, à utiliser avec les fonctions rvest habituelles. Cherchez d'abord du JSON dans les balises script ou un endpoint JSON, ce qui est généralement plus rapide.

RSelenium vaut-il encore la peine ?

RSelenium est toujours sur le CRAN, mais il nécessite Java, un serveur ou un driver Selenium et des versions de navigateur concordantes, ce qui rend l'installation fragile. Utilisez-le si votre équipe utilise déjà Selenium ou a besoin de Firefox. Pour la plupart des utilisateurs de R, read_html_live() dans rvest couvre le rendu, les clics et la saisie avec bien moins de configuration.

Comment scraper un tableau d'un site web en R ?

Chargez la page avec read_html(), sélectionnez le tableau avec html_element() et un sélecteur CSS comme "table.data", puis appelez html_table(). Vous obtenez un tibble avec une colonne par colonne du tableau. Utilisez header = FALSE pour les tableaux sans en-tête, et nettoyez ensuite les nombres avec readr::parse_number().

Comment éviter d'être bloqué quand on scrape avec R ?

Ralentissez avec req_throttle() de httr2, relancez les réponses 429 et 503 avec req_retry(), définissez un user agent honnête et mettez les pages en cache pour ne pas les récupérer deux fois. Si un site renvoie encore des erreurs 403 ou des pages de vérification, faites passer les requêtes par une API de scraping qui gère les proxys et le rendu, et gardez votre parseur rvest inchangé.

Comment stocker une clé d'API en R ?

Placez la clé dans votre fichier .Renviron utilisateur sous la forme d'une ligne comme WSA_API_KEY=your_key, redémarrez R et lisez-la avec Sys.getenv("WSA_API_KEY"). Ne collez jamais de clé dans des scripts, des notebooks ou des URL que vous partagez, et ajoutez tout fichier .Renviron de projet au .gitignore pour que la clé n'atteigne jamais le contrôle de version.

Peut-on scraper des pages protégées par une connexion avec R ?

Techniquement oui : session() de rvest peut soumettre des formulaires et conserver les cookies, et httr2 peut envoyer des requêtes authentifiées. Ne le faites qu'avec l'autorisation du propriétaire du site et dans le respect de ses conditions, car les espaces connectés contiennent généralement des données personnelles ou contractuelles. Si le site propose une API officielle, utilisez-la plutôt que de scraper les pages connectées.

Comment scraper de nombreuses pages en parallèle en R ?

Construisez une liste de requêtes httr2 et envoyez-les avec req_perform_parallel(), en utilisant max_active pour plafonner le nombre de requêtes simultanées. Ajoutez req_throttle() pour qu'un hôte ne reçoive jamais plus de trafic qu'il ne peut en absorber. Gardez une concurrence modérée pour chaque site, car la vitesse compte rarement autant que le fait de ne pas être bloqué.

Le web scraping en R est-il légal ?

Scraper des données publiquement accessibles est souvent légal, mais cela dépend des conditions du site, du type de données et du droit local, en particulier pour les données personnelles et les contenus protégés par le droit d'auteur. Le langage n'y change rien. Respectez le robots.txt, évitez les données personnelles sans base légale et faites-vous conseiller juridiquement pour les projets commerciaux.

Gardez votre code R, déléguez les pages difficiles

Vos parseurs rvest n'ont pas besoin de changer quand un site commence à vous bloquer ou exige JavaScript. Pointez la récupération vers le Scraper API de WebScrapingAPI et continuez d'analyser en R. Le rendu JavaScript et le géociblage ne coûtent aucun crédit supplémentaire, et les requêtes échouées ne sont pas facturées. Comparez les offres Scraper API, à commencer par un essai gratuit de 5 000 requêtes.

À propos de l'auteur

Raluca Penciuc, Développeur full-stack @ WebScrapingAPI

Raluca Penciuc

Développeur full-stack

Raluca Penciuc est développeuse Full Stack chez WebScrapingAPI ; elle conçoit des robots de collecte de données, améliore les techniques de contournement et recherche des moyens fiables de réduire le risque de détection sur les sites cibles.

Web scraping avec AWS Lambda : guide pour Python et Java 2026
Guides

Web scraping avec AWS Lambda : guide pour Python et Java 2026

TL;DR: En bref : le web scraping avec AWS Lambda fonctionne mieux lorsque chaque invocation est courte, bien délimitée et peut faire l'objet d'une nouvelle tentative de manière indépendante. Commencez par utiliser les requêtes HTTP directes, AWS SAM et S3, puis ajoutez SQS, des conteneurs, le rendu dans un navigateur, des proxys ou une couche de récupération gérée uniquement lorsque la charge de travail démontre qu'elle en a besoin.

Suciu Dan33 min read
Lire l'article
Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données
Guides

Comment utiliser GoSpider : exploration, nettoyage des URL et extraction de données

TL;DR: En bref : GoSpider est un robot d'exploration en ligne de commande destiné à découvrir des URL, et non un outil complet de collecte de données structurées. Ce guide d'utilisation de GoSpider présente comment effectuer une exploration limitée, gérer correctement les résultats, transférer les données de Colly vers un fichier CSV, ainsi qu'une procédure de diagnostic pour les réponses 403 ou les pages nécessitant un rendu JavaScript.

Suciu Dan24 min read
Lire l'article
Comment récupérer les données de Redfin : Guide Python des données immobilières
Guides

Comment récupérer les données de Redfin : Guide Python des données immobilières

TL;DR: Redfin expose des points d'extrémité d'API cachés qui renvoient du JSON structuré pour les listes de propriétés, ce qui permet d'ignorer complètement l'analyse HTML fragile. Ce guide vous accompagne dans la construction d'un scraper Python qui extrait les données de location et de vente, effectue des recherches par emplacement, surveille les nouvelles inscriptions via des sitemaps XML et exporte des résultats propres au format CSV ou JSON.

Suciu Dan15 min read
Lire l'article

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.