Aller au contenu
Retour au blog

Le web scraping avec Ruby : guide étape par étape avec des exemples de code fonctionnels

Raluca PenciucDernière mise à jour le 20 min read
Le web scraping avec Ruby : guide étape par étape avec des exemples de code fonctionnels
En bref : Ruby effectue du scraping sur le Web à l'aide de Faraday pour le protocole HTTP, de Nokogiri pour l'analyse du CSS et des expressions XPath, et d'un petit pool de threads pour la concurrence. Ce guide explique comment créer un scraper qui explore un catalogue de 1 000 livres au format CSV et JSON, effectue des tentatives de récupération de manière courtoise, gère une page JavaScript avec Ferrum et appelle une API de scraping à partir du même code.

Le scraping Web avec Ruby repose sur trois éléments : Faraday récupère la page, Nokogiri analyse le code HTML à l’aide de sélecteurs CSS ou d’expressions XPath, et un pool fixe de threads récupère plusieurs pages à la fois sans surcharger le site. Vous allez créer un scraper de A à Z. Il collecte tous les livres de books.toscrape.com, un catalogue d’entraînement public, suit la pagination, traite les pages en parallèle, réessaie correctement les requêtes soumises à une limite de débit et écrit les résultats au format CSV et JSON. Vous traiterez ensuite une page rendue en JavaScript (quotes.toscrape.com/js/) de trois manières différentes, vous vous connecterez à un formulaire de cabinet à l’aide de Mechanize, et vous appellerez une API de scraping à partir du même code Faraday.

Le code est destiné à Ruby 3.4 ou une version plus récente, ainsi qu’aux versions des gemmes indiquées dans le tableau ci-dessous.

Ruby est-il adapté au web scraping ?

Oui. Ruby dispose de gems éprouvées pour chaque étape d’un scraper : Faraday pour récupérer les pages, Nokogiri pour les analyser, Ferrum pour piloter Chrome en mode headless, et Mechanize pour les formulaires et les sessions. Le code reste concis et lisible, et s'intègre naturellement si votre application fonctionne déjà sous Ruby ou Rails. Python dispose d'un écosystème de scraping plus vaste et de davantage d'outils pour les navigateurs.

Optez pour Ruby lorsque :

  • Votre application, votre équipe ou votre file d’attente de tâches fonctionne déjà sous Ruby ou Rails, et vous souhaitez que le scraper fasse partie de la même base de code.
  • Vous souhaitez un scraper lisible utilisant quelques gemmes bien connues plutôt qu’un gros framework.

Optez pour Python lorsque :

  • Vous avez besoin d’une bibliothèque ou d’un outil de navigateur pour lequel il n’existe pas d’équivalent en Ruby. Notre guide sur le web scraping avec Python aborde cette approche.
  • Votre équipe travaille principalement dans des notebooks de données et souhaite que le scraper soit intégré à l'analyse.

De quels gemmes Ruby avez-vous besoin pour le web scraping ?

Vous avez besoin d’un client HTTP et d’un analyseur HTML. Faraday et Nokogiri remplissent ces deux fonctions, et quelques petits gems permettent d’ajouter des tentatives de reconnexion, la sortie au format CSV et un navigateur lorsque la page le nécessite.

Tâche

Gem

Version vérifiée en novembre 2026

Quand l'utiliser

Client HTTP

faraday

2.15.1

Choix par défaut, avec un middleware pour les tentatives de reconnexion et la gestion des erreurs

Nouvelles tentatives

faraday-retry

2.4.0

Nouvelles tentatives avec délai d'attente. Définissez vous-même les codes d'état

Client HTTP (alternative)

httparty

0.24.3

Requêtes ponctuelles rapides. Analyse les réponses JSON à votre place

Analyse HTML

nokogiri

1.19.7

CSS et XPath, précompilés pour les plateformes courantes

CSV

csv

3.3.7

Gem intégrée depuis Ruby 3.4, ajoutez-la donc à votre Gemfile

JSON

json

Gem par défaut

JSON.pretty_generate pour des fichiers lisibles

Concurrence

Thread + Queue

Bibliothèque standard

Un pool de workers limité

Chrome sans interface graphique

ferrum

0.20.1

Pilote Chrome via le protocole DevTools

Automatisation du navigateur (alternative)

selenium-webdriver + capybara

4.52.0 et 3.40.0

Multinavigateur, basé sur WebDriver

Formulaires et sessions

mechanize

2.14.2

Cookies, liens et soumission de formulaires

Framework de crawling

kimurai

2.2.0 (publié en janvier 2026)

Des robots d'indexation et des moteurs de navigation réunis dans un seul framework

Tests

minitest + webmock

6.0.6 et 3.34.0

Tests de parseurs hors ligne avec des requêtes simulées

La dernière version de Watir date d’août 2023 ; privilégiez donc Ferrum ou Selenium dans vos nouveaux codes. Pour comparer d’autres analyseurs HTML Ruby que Nokogiri, consultez notre guide des bibliothèques Ruby pour l’analyse de HTML et XML.

Comment mettre en place un projet de web scraping en Ruby

Un projet de scraping en Ruby nécessite Ruby 3.4 ou une version plus récente, Bundler, ainsi qu'un Gemfile qui fixe les gemmes ci-dessus. La configuration ne nécessite que quelques commandes.

Commencez par installer Ruby si vous ne disposez pas d’une version à jour, puis vérifiez-la :

ruby -v

Ruby 4.0 est la série stable actuelle, et la version 3.4 fonctionne également. Ruby 3.3 ne reçoit plus que des correctifs de sécurité, et Ruby 3.2 a atteint sa fin de vie en avril 2026.

Créez le projet et un fichier Gemfile :

mkdir book_scraper && cd book_scraper
bundle init

Remplacez le Gemfile généré par celui-ci :

# frozen_string_literal: true

source "https://rubygems.org"

ruby ">= 3.4"

gem "faraday", "~> 2.15"
gem "faraday-retry", "~> 2.4"
gem "nokogiri", "~> 1.19"
gem "csv", "~> 3.3"

group :browser do
  gem "ferrum", "~> 0.20"
end

group :test do
  gem "minitest", "~> 6.0"
  gem "webmock", "~> 3.34"
end

Les ~> pins acceptent les versions mineures, mais pas la prochaine version majeure. Ferrum se trouve dans un :browser groupe car seule la section JavaScript en a besoin. Installez tout :

bundle install

Le scraper se trouve dans lib/book_scraper.rb, et les petits scripts dans scripts/ l'appellent. Exécutez chacun d'entre eux avec bundle exec ruby scripts/<name>.rb.

Pourquoi require "csv" échoue-t-il ?

require "csv" échoue-t-il ?

Depuis Ruby 3.4, csv est un gem intégré au lieu d’un gem par défaut ; sous Bundler, il ne se charge donc que si votre Gemfile le mentionne. Sans cela, bundle exec affiche cet avertissement :

warning: csv used to be loaded from the standard library, but is not part of the default gems since Ruby 3.4.0.
You can add csv to your Gemfile or gemspec to fix this error.

Puis il s'arrête avec cannot load such file -- csv (LoadError). La gem "csv", "~> 3.3" ligne du Gemfile ci-dessus permet de résoudre ce problème. En dehors de ruby script.rb en dehors de Bundler détecte toujours csv, ce qui explique pourquoi l’erreur n’apparaît souvent qu’une fois qu’un projet dispose d’un Gemfile.

Comment envoyer votre première requête HTTP avec Faraday

Faraday envoie la requête et renvoie une réponse avec un statut, des en-têtes et un corps. Commencez par une ligne dans irb pour vérifier que le site répond :

require "faraday"
Faraday.get("https://books.toscrape.com/").status # => 200

Un véritable scraper a besoin d’une connexion réutilisable qui identifie votre client, cesse d’attendre les serveurs lents, signale les erreurs en cas de réponses échouées et effectue des tentatives de réessai lorsque le serveur vous demande de ralentir. Apprenez à définir des en-têtes de requête réalistes avant de scraper des sites qui les vérifient.

Créez lib/book_scraper.rb et lancez le module :

# frozen_string_literal: true

require "faraday"
require "faraday/retry"
require "nokogiri"
require "uri"
require "csv"
require "json"

module BookScraper
  START_URL = "https://books.toscrape.com/catalogue/page-1.html"
  USER_AGENT = "BookScraper/1.0 (+mailto:you@example.com)"
  RATINGS = { "One" => 1, "Two" => 2, "Three" => 3, "Four" => 4, "Five" => 5 }.freeze

  module_function

  # One reusable Faraday connection: timeouts, retries, and errors for non-2xx.
  def client
    Faraday.new(headers: { "User-Agent" => USER_AGENT }) do |f|
      f.options.open_timeout = 5
      f.options.timeout = 15
      f.response :raise_error # outer: raises once retries are used up
      f.request :retry,
                max: 3,
                interval: 0.5,
                backoff_factor: 2,
                interval_randomness: 0.5,
                retry_statuses: [429, 500, 502, 503, 504],
                max_interval: 30 # wait no longer than this per retry
    end
  end

  def fetch(conn, url)
    response = conn.get(url)
    # No charset in Content-Type, so Faraday returns binary; tag it UTF-8 for string methods
    response.body.force_encoding(Encoding::UTF_8)
  end

Voici le rôle de chaque élément :

  • L'en-tête User-Agent nomme votre scraper et fournit une adresse de contact, afin que les propriétaires de sites puissent vous joindre. Remplacez you@example.com par une adresse réelle.
  • open_timeout et timeout limitez le temps d'attente pour la connexion (5 secondes) et la lecture (15 secondes), afin qu'une page lente ne bloque pas l'exécution.
  • raise_error transforme les réponses 4xx et 5xx en exceptions telles que Faraday::ResourceNotFound, de sorte qu’une page manquante soit clairement signalée.
  • retry réessaie les réponses limitées en débit et les erreurs de serveur en augmentant progressivement les délais d’attente. La section consacrée aux réessais explique chaque option et pourquoi l’ordre de ces deux lignes est important.

La fetch méthode renvoie le corps balisé en UTF-8. Lorsque l’en-tête Content-Type ne spécifie pas de jeu de caractères, comme sur ce site, Faraday renvoie une chaîne binaire (ASCII-8BIT). Nokogiri lit toujours le jeu de caractères indiqué dans les balises meta de la page, mais ce n’est pas le cas de vos propres vérifications de chaîne. L’appel de include?("£") sur le corps binaire déclenche une Encoding::CompatibilityError: incompatible character encodings: BINARY (ASCII-8BIT) and UTF-8. Le balisage du corps avec force_encoding résout ce problème. La documentation de Faraday traite plus en détail des adaptateurs, des intergouvernements et des options de connexion.

Comment analyser du code HTML avec Nokogiri

Nokogiri transforme une chaîne HTML en un document que vous pouvez parcourir à l’aide de sélecteurs CSS ou XPath. css renvoie toutes les occurrences, at_css renvoie la première, [] lit un attribut, et text lit le contenu.

Sur books.toscrape.com, chaque livre se trouve dans une article.product_pod fiche. Testez le sélecteur irb avant de créer quoi que ce soit à partir de celui-ci :

doc = Nokogiri::HTML(Faraday.get("https://books.toscrape.com/catalogue/page-1.html").body)
doc.css("article.product_pod").size # => 20

Vingt cartes correspondent, une par livre. La même requête en XPath est doc.xpath("//article[@class=\"product_pod\"]"). Le CSS est plus court pour le balisage basé sur les classes, tandis que l’XPath permet d’effectuer des correspondances sur du texte et de remonter l’arborescence. Si vous hésitez entre les sélecteurs CSS et l’XPath, notre comparaison présente les avantages et inconvénients de chacun, et le tutoriel de recherche de Nokogiri aborde les requêtes mixtes. Tester dans irb au préalable signifie qu’un sélecteur défectueux ne coûte que quelques secondes, et non une exécution de 50 pages qui écrit un fichier vide.

Comment extraire des données structurées dans des hachages Ruby

Transformez chaque fiche en un hachage avec des valeurs typées : un titre de type String, un prix de type Float, une note de type Integer, un indicateur de disponibilité de type booléen et une URL absolue. Les valeurs typées permettent de trier, filtrer et exporter proprement. Ajoutez parse_books au module :

  def parse_books(html, page_url)
    doc = Nokogiri::HTML(html)
    doc.css("article.product_pod").map do |card|
      link = card.at_css("h3 a")
      rating_class = card.at_css("p.star-rating")["class"].split.last
      {
        title: link["title"],
        price: card.at_css("p.price_color").text.delete("^0-9.").to_f,
        rating: RATINGS.fetch(rating_class, nil),
        in_stock: card.at_css("p.availability").text.include?("In stock"),
        url: URI.join(page_url, link["href"]).to_s
      }
    end
  end

Pourquoi cette architecture :

  • Le titre provient de l'attribut title , car le texte visible est tronqué lorsque les titres sont longs.
  • delete("^0-9.") ne conserve que les chiffres et la virgule décimale, donc £51.77 devient 51.77 avant to_f.
  • La note est définie dans un nom de classe tel que star-rating Three, auquel le RATINGS hachage mappe à un entier.
  • URI.join résout les liens relatifs par rapport à l’URL de la page, de sorte que chaque ligne contient une URL complète.

Essayez-le sur la première page avec scripts/first_page.rb:

require_relative "../lib/book_scraper"

conn = BookScraper.client
html = BookScraper.fetch(conn, BookScraper::START_URL)
books = BookScraper.parse_books(html, BookScraper::START_URL)

puts "books on page 1: #{books.size}"
puts JSON.pretty_generate(books.first(2))
puts "next: #{BookScraper.next_page_url(html, BookScraper::START_URL)}"

La dernière ligne utilise next_page_url, que vous ajouterez dans la section suivante. Une fois en place, le script affiche :

books on page 1: 20
[
  {
    "title": "A Light in the Attic",
    "price": 51.77,
    "rating": 3,
    "in_stock": true,
    "url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
  },
  {
    "title": "Tipping the Velvet",
    "price": 53.74,
    "rating": 1,
    "in_stock": true,
    "url": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html"
  }
]
next: https://books.toscrape.com/catalogue/page-2.html

Comment gérer la pagination en Ruby

Pour paginer, lisez le lien « suivant » sur chaque page, convertissez-le en URL complète, et continuez jusqu’à ce que le lien disparaisse. Ajoutez un délai entre les pages et une limite de pages, afin qu’un site en boucle ne puisse pas faire tourner votre scraper indéfiniment.

Le catalogue marque la page suivante avec li.next a. Ajoutez ces deux méthodes :

  def next_page_url(html, page_url)
    href = Nokogiri::HTML(html).at_css("li.next a")&.[]("href")
    href && URI.join(page_url, href).to_s
  end

  # Follow "next" links until there are none, with a delay and a page cap.
  def crawl(conn, start_url = START_URL, max_pages: 100, delay: 0.5)
    url = start_url
    books = []
    pages = 0
    while url && pages < max_pages
      html = fetch(conn, url)
      books.concat(parse_books(html, url))
      pages += 1
      url = next_page_url(html, url)
      sleep(delay) if url
    end
    [books, pages]
  end

Sur la dernière page, il n’y a pas de lien ; l’opérateur de navigation sécurisée (&.) renvoie nil et la boucle se termine. La max_pages: 100 limiteur vous protège contre les boucles, et la demi-seconde sleep protège le site contre les pics de trafic. Sur ce catalogue, crawl 50 pages sont consultées et 1 000 livres sont renvoyés, que la section d’exportation enregistre dans des fichiers.

Suivre les liens d’une page à l’autre vous fait passer du scraping au crawling. Notre guide sur le scraping et le crawling explique où se situe la frontière.

Comment extraire plusieurs pages simultanément en Ruby

Lorsque vous connaissez déjà les URL des pages, un petit pool de threads récupère plusieurs pages à la fois. Les threads Ruby sont ici très utiles, car l’interpréteur libère son verrou global pendant qu’un thread attend une réponse sur le réseau. Une taille de pool fixe permet de maintenir une charge prévisible sur le site.

Les URL des pages du catalogue suivent un modèle ; vous pouvez donc créer la liste à l’avance et l’ajouter crawl_concurrently au module :

  # Fetch a known list of pages with a fixed number of worker threads.
  def crawl_concurrently(urls, workers: 4)
    queue = Queue.new
    urls.each { |u| queue << u }
    queue.close
    results = Queue.new
    errors = Queue.new

    threads = Array.new(workers) do
      Thread.new do
        conn = client # one connection per thread
        while (url = queue.pop)
          begin
            results << parse_books(fetch(conn, url), url)
          rescue Faraday::Error => e
            errors << [url, e.class.name]
          end
        end
      end
    end
    threads.each(&:join)

    books = []
    books.concat(results.pop) until results.empty?
    failed = []
    failed << errors.pop until errors.empty?
    [books, failed]
  end

Comment les différents éléments s'articulent :

  • Un Queue met fin proprement aux threads de travail. Une fois close, pop renvoie les URL restantes, puis nil, ce qui met fin à chaque boucle, sans qu’aucune valeur sentinelle ne soit nécessaire. Consultez la documentation sur Thread::Queue.
  • Chaque thread établit sa propre connexion ; ainsi, aucun état de connexion n’est partagé entre les threads.
  • Les erreurs sont interceptées au niveau de chaque page. Le rescue enregistre l’URL ayant échoué et passe à la suivante ; ainsi, une seule page défectueuse n’arrête pas le lot.
  • Queue est « thread-safe » : les workers envoient donc leurs résultats sans avoir besoin d’un mutex.

Exécutez-le sur les 50 pages ainsi que sur une URL inexistante, à l’aide de scripts/scrape_concurrently.rb:

require_relative "../lib/book_scraper"

urls = (1..50).map { |n| "https://books.toscrape.com/catalogue/page-#{n}.html" }
urls << "https://books.toscrape.com/catalogue/page-999.html" # this page doesn't exist

books, failed = BookScraper.crawl_concurrently(urls, workers: 4)
puts "books: #{books.size}, failed: #{failed.inspect}"

Le résultat affiche tous les livres ainsi que la page qui a échoué :

books: 1000, failed: [["https://books.toscrape.com/catalogue/page-999.html", "Faraday::ResourceNotFound"]]

Les résultats arrivent dans l’ordre où les threads se terminent ; triez-les donc si l’ordre a de l’importance. Limitez workers modéré sur les sites qui ne vous appartiennent pas : le pool sert justement à limiter la charge.

Comment réessayer les requêtes ayant échoué de manière « polie »

Par défaut, faraday-retry ne réessaie que les délais d’expiration et les échecs de connexion, sans délai d’attente entre les tentatives et sans tenir compte des codes d’état HTTP. Un code 429 « Too Many Requests » ou un code 503 « Service Unavailable » échoue dès la première tentative, à moins que vous ne configuriez vous-même le middleware.

Ces options de la client méthode permettent de faire fonctionner les réessais :

  • retry_statuses: [429, 500, 502, 503, 504] indique au middleware quelles réponses réessayer. Sans cela, aucune ne l'est.
  • max: 3 autorise trois tentatives après la première.
  • interval: 0.5, backoff_factor: 2, et interval_randomness: 0.5 commence par un délai d’attente d’une demi-seconde, le double à chaque fois et ajoute une variation aléatoire.
  • max_interval: 30 limite chaque délai d’attente à 30 secondes.

Lorsqu’une réponse réessayée comporte un Retry-After en-tête, le middleware attend aussi longtemps que le serveur le demande. Si ce délai est supérieur à max_interval, il abandonne immédiatement : un serveur qui répond Retry-After: 120 reçoit une Faraday::TooManyRequestsError de la part de ce client. Déclenchez une max_interval si vous préférez attendre. Seules les méthodes idempotentes telles que GET font l’objet d’une nouvelle tentative, et une erreur 404 n’en fait jamais l’objet.

L'ordre des deux lignes de middleware a également son importance. Enregistrez f.response :raise_error avant f.request :retry. Faraday exécute le middleware enregistré en premier à l’extérieur, de sorte que retry il voit la réponse brute 429 ou 503, effectue une nouvelle tentative, et raise_error ne se déclenche qu’une fois les tentatives épuisées. Intervertissez les deux lignes et raise_error transforme le 429 en un Faraday::TooManyRequestsError avant retry ne voie jamais de code d’état, de sorte qu’aucune tentative de réessai n’ait lieu, même avec retry_statuses soit défini.

Avec cette configuration, un serveur qui renvoie deux fois un 429 puis un 200 renvoie à votre code un 200 normal. Un serveur qui ne cesse de renvoyer un 503 reçoit quatre requêtes, puis votre code reçoit Faraday::ServerError. Le fichier README de faraday-retry répertorie toutes les options, y compris retry_block celles permettant de consigner les tentatives.

Comment enregistrer les données extraites au format CSV et JSON

Écrivez le fichier CSV avec une liste d’en-têtes explicite et le fichier JSON avec JSON.pretty_generate. Les deux formats conservent les valeurs typées issues de l’étape d’analyse.

Ajoutez les méthodes d’exportation :

  HEADERS = %w[title price rating in_stock url].freeze

  def write_csv(books, path)
    CSV.open(path, "w", write_headers: true, headers: HEADERS) do |csv|
      books.each { |b| csv << b.values_at(*HEADERS.map(&:to_sym)) }
    end
  end

  def write_json(books, path)
    File.write(path, JSON.pretty_generate(books))
  end

values_at récupère les champs dans l’ HEADERS , de sorte que les lignes s'alignent avec l'en-tête même si vous ajoutez des clés ultérieurement. Associez l'exploration et les exportations dans scripts/scrape_books.rb:

require_relative "../lib/book_scraper"

books, pages = BookScraper.crawl(BookScraper.client)
BookScraper.write_csv(books, "output/books.csv")
BookScraper.write_json(books, "output/books.json")
puts "pages: #{pages}, books: #{books.size}"

Créez le dossier de sortie et exécutez le script :

mkdir -p output
bundle exec ruby scripts/scrape_books.rb

Le script affiche pages: 50, books: 1000. output/books.csv comporte une ligne d'en-tête plus 1 000 lignes de données, et commence ainsi :

title,price,rating,in_stock,url
A Light in the Attic,51.77,3,true,https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html

output/books.json contient les mêmes 1 000 enregistrements sous la forme d’un tableau d’objets. Les notes de mise à jour de Ruby 3.4 mentionnent csv parmi les gemmes désormais intégrées au paquetage, c’est donc à cette étape qu’apparaît une ligne manquante dans le Gemfile. Fermez le module avec une dernière méthode end après la dernière méthode.

Comment tester un scraper Ruby sans accéder au site

Enregistrez une page réelle en tant que fixture, simulez la requête avec WebMock et vérifiez le résultat analysé. Ces tests s’exécutent hors ligne et détectent les dérives de sélecteurs avant qu’une tâche planifiée ne génère un fichier CSV vide.

Enregistrez la première page du catalogue :

mkdir -p test/fixtures
curl -o test/fixtures/page-1.html https://books.toscrape.com/catalogue/page-1.html

Puis écrivez test/book_scraper_test.rb:

require "minitest/autorun"
require "webmock/minitest"
require_relative "../lib/book_scraper"

class BookScraperTest < Minitest::Test
  PAGE = "https://books.toscrape.com/catalogue/page-1.html"

  def setup
    stub_request(:get, PAGE).to_return(status: 200, body: File.read(File.join(__dir__, "fixtures/page-1.html")))
  end

  def test_parses_twenty_books_with_typed_fields
    books = BookScraper.parse_books(BookScraper.fetch(BookScraper.client, PAGE), PAGE)
    assert_equal 20, books.size
    assert_equal "A Light in the Attic", books.first[:title]
    assert_in_delta 51.77, books.first[:price]
    assert_equal 3, books.first[:rating]
  end

  def test_finds_next_page
    html = BookScraper.fetch(BookScraper.client, PAGE)
    assert_equal "https://books.toscrape.com/catalogue/page-2.html", BookScraper.next_page_url(html, PAGE)
  end

  def test_retries_503_then_succeeds
    stub_request(:get, "https://example.test/flaky").to_return({ status: 503 }, { status: 200, body: "<p>ok</p>" })
    assert_equal 200, BookScraper.client.get("https://example.test/flaky").status
  end
end

Exécutez les tests :

bundle exec ruby test/book_scraper_test.rb

La ligne de résumé indique 3 runs, 6 assertions, 0 failures, 0 errors, 0 skips. Les deux premiers tests vérifient le parseur et la pagination. Le troisième simule un code d’erreur 503 suivi d’un 200 et vérifie que le client renvoie bien le 200 ; il échoue donc si quelqu’un inverse l’ordre des middlewares ou supprime retry_statuses. WebMock bloque également les appels réseau réels ; ainsi, l’absence d’un stub apparaît clairement comme une erreur.

Comment extraire des pages rendues en JavaScript en Ruby

Faraday et Nokogiri ne voient que le code HTML envoyé par le serveur. Si la page construit son contenu à l’aide de JavaScript dans le navigateur, ce contenu est absent de la réponse. Vérifiez d’abord, puis choisissez la solution la plus légère : des données intégrées, puis un navigateur sans interface graphique, puis une API gérée.

Comment vérifier si une page nécessite du JavaScript

Récupérez la page et comptez les éléments attendus. quotes.toscrape.com/js/ affiche 10 citations dans un navigateur. Comptez-les dans le code HTML brut :

require "faraday"
require "nokogiri"
require "json"
url = "https://quotes.toscrape.com/js/"
html = Faraday.get(url, nil, "User-Agent" => "BookScraper/1.0").body.force_encoding("UTF-8")
puts "div.quote in raw HTML: #{Nokogiri::HTML(html).css('div.quote').size}"
# The page ships its data in an inline script: var data = [ ... ];
json = html[/var data = (\[.*?\]);/m, 1]
quotes = JSON.parse(json).map do |q|
  { text: q["text"], author: q.dig("author", "name"), tags: q["tags"] }
end
puts "quotes from embedded JSON: #{quotes.size}"
puts JSON.pretty_generate(quotes.first)

La première ligne du résultat est div.quote in raw HTML: 0: les citations ne figurent pas dans le code HTML du serveur. Vous pouvez constater la même chose dans le navigateur. L’option « Afficher la source » montre le code HTML du serveur, tandis que le panneau « Éléments » des DevTools affiche la page après l’exécution de JavaScript.

Extraire d’abord le JSON intégré

De nombreuses pages JavaScript transmettent leurs données dans un script intégré et les affichent dans le navigateur. Lire ces données directement est plus léger et plus fiable que de lancer un navigateur. La seconde partie du script ci-dessus fait exactement cela : l’expression régulière capture le tableau attribué à var data, puis JSON.parse le transforme en hachages Ruby.

Le script affiche ensuite :

quotes from embedded JSON: 10
{
  "text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
  "author": "Albert Einstein",
  "tags": [
    "change",
    "deep-thoughts",
    "thinking",
    "world"
  ]
}

Voilà les 10 citations, avec leurs balises, et sans passer par un navigateur. Le même principe s’applique lorsqu’une page charge ses données depuis une API : repérez la réponse JSON dans l’onglet « Réseau » des DevTools et interrogez cette URL avec Faraday.

Afficher la page avec Ferrum

Lorsque les données n’existent qu’après l’exécution de JavaScript, affichez la page dans Chrome sans interface graphique. Ferrum pilote Chrome directement via le protocole DevTools ; il n’y a donc aucun pilote binaire à installer ni à synchroniser. Il nécessite Chrome ou Chromium sur votre PATH, ou un chemin d'accès dans la BROWSER_PATH variable d’environnement.

require "ferrum"
require "nokogiri"
browser = Ferrum::Browser.new(headless: true, timeout: 30)
begin
  page = browser.create_page
  page.go_to("https://quotes.toscrape.com/js/")
  page.wait_for_selector(css: "div.quote") # Ferrum 0.19+
  doc = Nokogiri::HTML(page.body)
  quotes = doc.css("div.quote").map do |q|
    { text: q.at_css("span.text").text, author: q.at_css("small.author").text }
  end
  puts "rendered div.quote: #{quotes.size}"
  puts quotes.first
ensure
  browser.quit
end

Enregistrez-le sous le nom scripts/ferrum_quotes.rb et exécutez-le avec bundle exec ruby scripts/ferrum_quotes.rb. Il affiche rendered div.quote: 10 suivi de la première citation. wait_for_selector se met en attente jusqu’à ce que les citations soient présentes, ce qui est plus efficace qu’un sleep. page.body renvoie le code HTML rendu, de sorte que le code Nokogiri est identique à celui d’une page statique, et ensure ferme Chrome même en cas d’échec.

Chaque instance de navigateur utilise de la mémoire physique et du CPU ; veillez donc à limiter la concurrence entre les navigateurs et à n’afficher que les pages qui en ont besoin. La page du projet Ferrum documente les clics, la saisie et les captures d’écran.

Selenium, Capybara et autres options de navigateur

selenium-webdriver fonctionne via WebDriver ; c’est donc le choix à privilégier lorsque vous avez besoin de Firefox ou d’un code multi-navigateur, et Capybara y ajoute un DSL plus convivial pour cliquer et remplir des champs. Cuprite est un pilote Capybara basé sur Ferrum, utile si vous écrivez déjà du code Capybara. Kimurai intègre les navigateurs et Nokogiri dans un framework complet, présenté ci-dessous. Si vous avez besoin de rendu, de temps d’attente et de captures d’écran, et que vous préférez afficher les pages sans exécuter de navigateurs sur vos propres serveurs, un navigateur hébergé se charge de cette tâche à votre place.

Comment soumettre des formulaires et conserver les sessions avec Mechanize

Mechanize se comporte comme un navigateur simple sans JavaScript. Il stocke les cookies, suit les redirections, et remplit et soumet les formulaires, ce qui convient aux connexions et aux flux en plusieurs étapes sur les sites avec rendu côté serveur.

Le site d’entraînement dispose d’un formulaire de connexion à l’adresse quotes.toscrape.com/login qui accepte n’importe quel nom d’utilisateur et mot de passe. Ajoutez gem "mechanize", "~> 2.14" à votre Gemfile, exécutez bundle install, puis connectez-vous :

require "mechanize"

agent = Mechanize.new
agent.user_agent = "BookScraper/1.0 (+mailto:you@example.com)"

page = agent.get("https://quotes.toscrape.com/login")
form = page.form_with(action: "/login")
form.username = "demo"
form.password = "demo"
home = agent.submit(form)

puts "logged in: #{!home.link_with(text: 'Logout').nil?}"
puts "quotes on page: #{home.search('div.quote').size}"

Le script affiche logged in: true et quotes on page: 10. form_with repère le formulaire grâce à son action ; les champs masqués, tels que le jeton CSRF, sont envoyés avec la requête, et le cookie de session reste dans le cache de l’agent pour les requêtes ultérieures. search exécute Nokogiri, les mêmes sélecteurs fonctionnent donc. Pour les connexions qui dépendent de JavaScript, utilisez plutôt Ferrum. N’automatisez les connexions que sur les sites où vous disposez d’un compte et des autorisations nécessaires.

Faut-il utiliser un framework Ruby de web scraping ?

La plupart des tâches n’en ont pas besoin. Faraday, Nokogiri et un pool de threads permettent de gérer quelques sites dotés d’une logique personnalisée, tout en vous laissant un contrôle total. Un framework s’avère utile lorsque vous exécutez de nombreux robots et que vous souhaitez centraliser la planification, les moteurs de navigateur et les pipelines en un seul endroit.

Kimurai est la principale option sous Ruby. La version 2.x est à nouveau maintenue et nécessite Ruby 3.2 ou une version plus récente. Avant de l’adopter, prenez en compte deux éléments :

  • Il comporte un arbre de dépendances important, comprenant notamment Capybara, Selenium et Active Support.
  • De nombreux tutoriels concernent encore Kimurai 1.x ; vérifiez donc que les exemples correspondent à l’API 2.x.

Faraday vs HTTParty

Ces deux clients fonctionnent. HTTParty est le moyen le plus rapide d’effectuer une requête ponctuelle et analyse pour vous les réponses JSON en hashes Ruby :

require "httparty"
require "nokogiri"

response = HTTParty.get(
  "https://books.toscrape.com/",
  headers: { "User-Agent" => "BookScraper/1.0 (+mailto:you@example.com)" },
  timeout: 15
)
doc = Nokogiri::HTML(response.body)
puts "status #{response.code}, books #{doc.css('article.product_pod').size}"

Cela affiche status 200, books 20. Faraday nécessite davantage de configuration, mais son middleware regroupe les réessais, les délais d’expiration et la gestion des erreurs au sein d’une seule connexion réutilisable. Utilisez HTTParty pour les scripts rapides et Faraday pour les scrapers qui s’exécutent selon un calendrier défini.

Quand le scraping Ruby « fait-on-soi-même » atteint-il ses limites ?

Le scraper présenté dans ce guide fonctionne car books.toscrape.com et quotes.toscrape.com sont des sites d’entraînement conçus pour les requêtes automatisées. Ce n’est pas le cas de nombreux sites en production. Le même code peut commencer à échouer face à des réponses 403, des pages CAPTCHA, des limites de débit par adresse IP, des vérifications d’empreinte digitale du navigateur ou du contenu qui varie selon les pays. Augmenter le nombre de threads n’y change rien, car ces limites dépendent de l’identité de l’utilisateur qui effectue la requête, et non de la vitesse à laquelle il la fait.

Symptôme

Cause probable

Solution à mettre en œuvre soi-même

Quand s’arrêter

Erreur 403 « Accès interdit » à chaque requête

Le site signale votre client ou votre adresse IP

Envoyez des en-têtes réalistes, ralentissez le débit

Le blocage persiste même à faible fréquence

429 Trop de requêtes

Limite de débit par adresse IP ou par session

Réduisez le nombre de workers, respectez la limite Retry-After

Vous avez besoin d'un débit supérieur à celui autorisé par une seule adresse IP

Une page CAPTCHA à la place du contenu

Détection des bots

Réduisez la fréquence des requêtes, vérifiez les conditions d'utilisation du site

Des CAPTCHA apparaissent sur le trafic normal

Fonctionne dans un navigateur, mais échoue dans Chrome « headless »

Vérification des empreintes numériques

Passez en revue les options de lancement en mode « headless »

Vous gérez les correctifs du navigateur

Contenu ou prix différents selon les régions

Réponses géolocalisées

Acheminement via une adresse IP située dans ce pays

Vous avez besoin de nombreux pays

Chaque solution « maison » implique une infrastructure supplémentaire à gérer et à surveiller : pools de proxys, parcs de navigateurs, files d’attente de réessais. Notre guide sur la manière d’éviter d’être bloqué aborde les en-têtes, la régulation du débit et les proxys. À un certain stade, il devient plus judicieux de déléguer la récupération des données et de conserver votre code Ruby pour l’analyse.

Comment appeler l’API Scraper depuis Ruby

L’API Scraper récupère la page pour vous, gère les proxys et les tentatives de récupération, et peut exécuter du JavaScript, tandis que votre code Ruby conserve les mêmes étapes Faraday et Nokogiri. Vous envoyez une requête GET avec votre clé API et l’URL cible, et ajoutez des options telles que render_js ou country uniquement lorsque la page en a besoin.

Ajoutez cette méthode au BookScraper module, avant le dernier end:

  # Scraper API: same Faraday, the target goes in params and gets URL-encoded for you.
  def scraper_api(url, render_js: false, country: nil)
    api = Faraday.new(url: "https://api.webscrapingapi.com") do |f|
      f.options.timeout = 120
    end
    params = { api_key: ENV.fetch("WSA_API_KEY"), url: url }
    params[:render_js] = 1 if render_js
    params[:country] = country if country
    api.get("/v2", params)
  end

Appelez-la ensuite depuis scripts/scraper_api.rb et vérifiez l'état avant de procéder à l'analyse :

require_relative "../lib/book_scraper"

response = BookScraper.scraper_api("https://quotes.toscrape.com/js/", render_js: true, country: "us")

if response.status == 200
  doc = Nokogiri::HTML(response.body)
  puts "quotes via Scraper API: #{doc.css('div.quote').size}"
else
  puts "Scraper API returned #{response.status}: #{response.body[0, 200]}"
end

Définissez votre clé dans le shell et exécutez-la :

export WSA_API_KEY="your_api_key"
bundle exec ruby scripts/scraper_api.rb

Pourquoi le code se présente-t-il ainsi ?

  • Le hachage « params » encode l'URL cible pour vous, donc son :, /, ?, et & ne peuvent pas interrompre la requête. Coller l’URL manuellement dans la chaîne de requête est une erreur courante.
  • ENV.fetch échoue rapidement. Si la variable n’est pas définie, Ruby lève une exception KeyError: key not found: "WSA_API_KEY" avant même qu’une requête ne soit envoyée.
  • Le délai d’expiration est plus long, car le rendu prend plus de temps qu’une simple récupération.
  • Cette connexion ne dispose d’aucun middleware de réessai. L’API effectue déjà des réessais de son côté ; les réessais du client ne feraient donc que répéter la tâche.
  • La branche d’erreur affiche le corps de la réponse. En cas de clé invalide, le script affiche Scraper API returned 401: {"error":"Invalid API key"}.

Avec HTTParty, le même appel transmet les options sous la forme query:, ce qui les encode de la même manière. Conservez la clé API côté serveur : ne la commitez pas, ne l’intégrez pas dans le code client et ne l’affichez pas dans les journaux.

Erreurs courantes de scraping en Ruby et comment les corriger

La plupart des erreurs de scraping Web avec Ruby proviennent d’un gem manquant, d’une configuration de réessai qui ne fonctionne pas, d’une incompatibilité d’encodage ou d’un contenu absent du code HTML. Voici chaque symptôme et sa solution.

Erreur ou symptôme

Cause

Solution

cannot load such file -- csv (LoadError)

csv est un gem intégré depuis Ruby 3.4

Ajoutez gem "csv" au Gemfile

Les réponses 429 ou 503 échouent lors de la première tentative

Les paramètres par défaut de faraday-retry n'incluent aucun code d'état

Définissez retry_statuses

Faraday::TooManyRequestsError malgré une configuration de nouvelle tentative

raise_error enregistrée après retry

Enregistrer raise_error première

La nouvelle tentative s'arrête immédiatement en cas de code 429

Retry-After est plus long que max_interval

Augmenter max_interval ou ralentir

Encoding::CompatibilityError lors des vérifications de chaîne

Corps binaire (pas de jeu de caractères dans Content-Type)

force_encoding("UTF-8")

Un sélecteur ne renvoie rien

Contenu généré par JavaScript ou sélecteur incorrect

Vérifiez le code HTML brut, puis consultez la section JavaScript

Faraday::ResourceNotFound pour une page d’un lot

Une erreur 404 générée par raise_error

Rescue Faraday::Error par page

KeyError: key not found: "WSA_API_KEY"

La variable d'environnement n'est pas définie

Exportez-la dans votre shell ou votre gestionnaire de secrets

URL relatives dans votre sortie

Brut href valeurs

Résolvez-les avec URI.join

403 Interdit

Blocage

Ralentissez, puis consultez la section « À faire soi-même »

Lorsqu'une erreur ne figure pas dans le tableau, affichez le code d'état et les quelques centaines de premiers octets du corps de la réponse. La plupart des bugs liés au scraping apparaissent clairement dès que vous voyez ce que le serveur a envoyé.

Comment pratiquer le scraping de manière responsable avec Ruby

Lisez le fichier robots.txt du site, qui respecte le protocole d’exclusion des robots (RFC 9309), et ignorez les chemins d’accès qu’il interdit à votre agent utilisateur. Limitez la taille de votre pool de workers, ajoutez des délais d’attente et respectez Retry-After lorsque le serveur vous demande de ralentir. Identifiez votre client à l’aide d’un User-Agent comprenant une adresse de contact. Respectez les conditions d’utilisation du site et évitez de collecter des données à caractère personnel, sauf si vous disposez d’une base juridique claire pour le faire. Pour connaître notre approche en la matière, consultez nos pratiques responsables en matière de données Web.

FAQ

Ruby est-il adapté au web scraping ?

Oui. Ruby dispose de gems éprouvées pour chaque étape : Faraday ou HTTParty pour récupérer les pages, Nokogiri pour analyser le HTML à l’aide de sélecteurs CSS ou d’XPath, Ferrum pour piloter Chrome en mode headless, et Mechanize pour les formulaires. Python dispose d’un écosystème de scraping plus vaste et de davantage d’outils de navigation, mais Ruby s’impose comme un choix naturel lorsque votre application ou votre équipe utilise déjà Ruby ou Rails.

Quelle est la meilleure bibliothèque Ruby pour le web scraping ?

La plupart des scrapers Ruby associent Faraday pour les requêtes HTTP à Nokogiri pour l'analyse. Ajoutez faraday-retry pour les tentatives de reconnexion, le gem csv pour les feuilles de calcul et la bibliothèque JSON intégrée pour les fichiers JSON. Utilisez Ferrum lorsqu’une page nécessite un véritable navigateur, Mechanize pour les formulaires et les connexions, et Kimurai si vous souhaitez un framework de crawling complet.

Dois-je utiliser Faraday ou HTTParty pour le web scraping ?

Les deux fonctionnent. HTTParty est le moyen le plus rapide d’effectuer une requête ponctuelle et analyse les réponses JSON à votre place. Faraday nécessite un peu plus de configuration, mais établit une connexion réutilisable avec un middleware, ce qui permet de centraliser les tentatives de reconnexion, les délais d’expiration et la gestion des erreurs. Pour un scraper fonctionnant selon un calendrier, le middleware de Faraday s’avère généralement très utile.

Nokogiri peut-il extraire des pages rendues en JavaScript ?

Non. Nokogiri analyse le code HTML envoyé par le serveur et n’exécute jamais de JavaScript ; le contenu généré par le navigateur est donc absent de la réponse. Vérifiez d’abord le code HTML brut, puis recherchez les données dans un script intégré ou dans une requête JSON effectuée par la page. Si ces données n’existent qu’après le rendu, utilisez Ferrum ou Selenium pour charger la page.

Peut-on faire du web scraping avec Ruby on Rails ?

Oui. Placez le scraper dans une classe Ruby simple ou un objet de service, puis exécutez-le à partir d’une tâche en arrière-plan avec Active Job afin que les requêtes web n’attendent jamais une exploration. Stockez les résultats avec Active Record, planifiez des exécutions récurrentes avec votre backend de tâches, et veillez à ce que le code HTTP et d’analyse reste testable en dehors de Rails grâce à des fixtures HTML enregistrées.

Kimurai reste-t-il un bon framework Ruby de web scraping ?

Kimurai 2.x est à nouveau maintenu et nécessite Ruby 3.2 ou une version plus récente ; c’est donc une option à envisager si vous souhaitez disposer de robots d’indexation, de moteurs de navigateur et de pipelines au sein d’un même framework. Il s’appuie sur un vaste arbre de dépendances, incluant notamment Capybara et Selenium. Pour les tâches plus modestes, Faraday, Nokogiri et un pool de threads vous offrent les mêmes résultats avec moins de composants.

Comment éviter d’être bloqué lors du scraping avec Ruby ?

Identifiez votre client avec un User-Agent clair, limitez la concurrence, ajoutez des délais entre les pages et réessayez en cas de réponses 429 et 503 tout en respectant l’en-tête Retry-After. Respectez le fichier robots.txt et les conditions d’utilisation du site. Les sites utilisant des CAPTCHA, l’empreinte numérique ou des limites de débit par adresse IP peuvent tout de même bloquer un scraper « bien élevé » ; c’est là que la rotation des adresses IP ou une API de scraping gérée s’avèrent utiles.

Et maintenant ?

Vous disposez désormais d’un scraper Ruby testé qui récupère, analyse, pagine, exécute les pages en parallèle, effectue des tentatives de réessai de manière courtoise et exporte des données propres. Quelques étapes supplémentaires suffisent pour le rendre prêt à être mis en production :

  • Exécutez-le via une tâche en arrière-plan. Sous Rails, intégrez le scraping dans un Active Job et planifiez-le, afin qu’aucune requête web n’ait à attendre la fin d’un scraping.
  • Ajoutez une limite de débit par hôte, afin que chaque worker partage un budget de requêtes commun au lieu de dépendre uniquement des temps de pause et de la taille du pool.
  • Ne générez le rendu que lorsque c’est nécessaire. Conservez Faraday pour les pages statiques et le JSON intégré, et envoyez uniquement les pages nécessitant du JavaScript à Ferrum.
  • Déléguez la récupération des données lorsque les blocs commencent. Dès que vous passez plus de temps sur les proxys et les CAPTCHA que sur l’analyse, transférez l’étape de récupération vers une API gérée et conservez le reste de votre code Ruby.

Conservez votre code Ruby, ignorez les blocs

Dirigez le même client Faraday vers l’API Scraper et récupérez la page déjà rendue, depuis le pays de votre choix, sans avoir à lancer vous-même de navigateurs ou de proxys. Commencez avec 5 000 requêtes gratuites. Aucun crédit supplémentaire n’est facturé pour le rendu JavaScript ou le ciblage géographique, et les requêtes ayant échoué ne sont pas facturées.

Consultez les tarifs de l’API Scraper

À 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.