Saltar al contenido
Empezar gratis
Volver al blog

Extracción de datos de la web con Ruby: guía paso a paso con código funcional

Raluca PenciucÚltima actualización el 24 min read
Extracción de datos de la web con Ruby: guía paso a paso con código funcional
En resumen: Ruby realiza el rastreo web con Faraday para HTTP, Nokogiri para el análisis de CSS y XPath, y un pequeño grupo de subprocesos para la concurrencia. Esta guía explica cómo crear un rastreador que recopila un catálogo de ejercicios con 1.000 libros en formatos CSV y JSON, realiza reintentos de forma cortés, gestiona una página con JavaScript mediante Ferrum y llama a una API de rastreo desde el mismo código.

El rastreo web con Ruby requiere tres componentes: Faraday recupera la página, Nokogiri analiza el HTML con selectores CSS o XPath, y un grupo fijo de subprocesos recupera muchas páginas a la vez sin sobrecargar el sitio. Crearás un rastreador de principio a fin. Este recopila todos los libros de books.toscrape.com, un catálogo público de práctica, sigue la paginación, procesa las páginas de forma simultánea, reintenta correctamente las solicitudes con límite de frecuencia y escribe los resultados en formatos CSV y JSON. A continuación, gestionarás una página renderizada con JavaScript (quotes.toscrape.com/js/) de tres formas diferentes, iniciarás sesión en un formulario de práctica con Mechanize y llamarás a una API de scraping desde el mismo código de Faraday.

El código está pensado para Ruby 3.4 o posterior y las versiones de las gemas que se indican en la tabla siguiente.

¿Es Ruby adecuado para el scraping web?

Sí. Ruby cuenta con gemas consolidadas para cada paso de un rastreador: Faraday para recuperar páginas, Nokogiri para analizarlas, Ferrum para controlar Chrome sin interfaz gráfica y Mechanize para formularios y sesiones. El código es breve y legible, y encaja de forma natural si tu aplicación ya se ejecuta en Ruby o Rails. Python cuenta con un ecosistema de scraping más amplio y más herramientas para navegadores.

Elige Ruby cuando:

  • Tu aplicación, equipo o cola de tareas ya se ejecute en Ruby o Rails, y quieras que el rastreador utilice la misma base de código.
  • Prefieras un rastreador legible con unas pocas gemas conocidas en lugar de un gran marco de trabajo.

Elige Python cuando:

  • Necesites una biblioteca o una herramienta de navegador que no tenga equivalente en Ruby. Nuestra guía sobre web scraping con Python aborda esa opción.
  • Tu equipo trabaja principalmente con cuadernos de datos y quiere que el rastreador esté junto al análisis.

¿Qué gemas de Ruby necesitas para el scraping web?

Necesitas un cliente HTTP y un analizador de HTML. Faraday y Nokogiri cubren ambas funciones, y unas cuantas gemas pequeñas añaden reintentos, salida en CSV y un navegador cuando una página lo requiere.

Tarea

Gema

Versión comprobada en noviembre de 2026

Cuándo utilizarla

Cliente HTTP

faraday

2.15.1

Opción predeterminada, con middleware para reintentos y errores

Reintentos

faraday-retry

2.4.0

Reintentos con retardo. Configura tú mismo los códigos de estado

Cliente HTTP (alternativa)

httparty

0.24.3

Solicitudes rápidas puntuales. Analiza las respuestas JSON por ti

Análisis de HTML

nokogiri

1.19.7

CSS y XPath, precompilados para las plataformas más habituales

CSV

csv

3.3.7

Gema incluida desde Ruby 3.4, así que añádela a tu Gemfile

JSON

json

Gema predeterminada

JSON.pretty_generate para archivos legibles

Concurrencia

Thread + Queue

Biblioteca estándar

Un grupo de trabajadores acotado

Chrome sin interfaz gráfica

ferrum

0.20.1

Controla Chrome a través del protocolo DevTools

Automatización del navegador (alternativa)

selenium-webdriver + capybara

4.52.0 y 3.40.0

Multinavegador, basado en WebDriver

Formularios y sesiones

mechanize

2.14.2

Cookies, enlaces y envío de formularios

Marco de rastreo

kimurai

2.2.0 (lanzado en enero de 2026)

Rastreadores y motores de navegador en un único marco

Pruebas

minitest + webmock

6.0.6 y 3.34.0

Pruebas de analizador sin conexión con solicitudes simuladas

La última versión de Watir data de agosto de 2023, por lo que es preferible utilizar Ferrum o Selenium en el código nuevo. Para comparar analizadores HTML de Ruby más allá de Nokogiri, consulta nuestra guía de bibliotecas de Ruby para analizar HTML y XML.

Cómo configurar un proyecto de web scraping en Ruby

Un proyecto de scraping en Ruby requiere Ruby 3.4 o una versión posterior, Bundler y un Gemfile que fije las gemas mencionadas anteriormente. La configuración se realiza con unos pocos comandos.

En primer lugar, instala Ruby si no tienes una versión actual y, a continuación, comprueba que funciona:

ruby -v

Ruby 4.0 es la serie estable actual, y la 3.4 también funciona. Ruby 3.3 solo recibe correcciones de seguridad, y Ruby 3.2 llegó al final de su vida útil en abril de 2026.

Crea el proyecto y un Gemfile:

mkdir book_scraper && cd book_scraper
bundle init

Sustituye el Gemfile generado por este:

# 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

El ~> pins admiten versiones menores, pero no la próxima versión principal. Ferrum se encuentra en un :browser grupo porque solo la sección de JavaScript lo necesita. Instala todo:

bundle install

El scraper se encuentra en lib/book_scraper.rb, y los pequeños scripts en scripts/ lo invocan. Ejecuta cada uno con bundle exec ruby scripts/<name>.rb.

¿Por qué require "csv" fallar?

require "csv" fallar?

Desde Ruby 3.4, csv es una gema incluida en el paquete en lugar de una gema predeterminada, por lo que, con Bundler, solo se carga si aparece en tu Gemfile. Sin ella, bundle exec muestra esta advertencia:

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.

A continuación, se detiene con cannot load such file -- csv (LoadError). La gem "csv", "~> 3.3" línea del Gemfile anterior lo soluciona. Fuera ruby script.rb fuera de Bundler sigue encontrando csv, por lo que el error suele aparecer solo cuando un proyecto cuenta ya con un Gemfile.

Cómo enviar tu primera solicitud HTTP con Faraday

Faraday envía la solicitud y devuelve una respuesta con un estado, encabezados y cuerpo. Empieza con una línea en irb para comprobar que el sitio responde:

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

Un rastreador real necesita una conexión reutilizable que identifique a tu cliente, deje de esperar a servidores lentos, genere errores ante respuestas fallidas y vuelva a intentarlo cuando el servidor te pida que reduzcas la velocidad. Aprende a configurar encabezados de solicitud realistas antes de rastrear sitios que los comprueben.

Crea lib/book_scraper.rb y ejecuta el módulo:

# 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

Esto es lo que hace cada parte:

  • El User-Agent da nombre a tu rastreador y proporciona una dirección de contacto, para que los propietarios de los sitios web puedan ponerse en contacto contigo. Sustituye you@example.com por una dirección real.
  • open_timeout y timeout limita el tiempo de espera para conectarse (5 segundos) y para leer (15 segundos), de modo que una página lenta no bloquee la ejecución.
  • raise_error convierte las respuestas 4xx y 5xx en excepciones como Faraday::ResourceNotFound, de modo que una página que no se encuentre se detecte claramente.
  • retry Reintenta las respuestas con limitación de velocidad y de error del servidor aumentando progresivamente los tiempos de espera. La sección sobre reintentos explica cada opción y por qué es importante el orden de estas dos líneas.

El fetch método devuelve el cuerpo etiquetado como UTF-8. Cuando el encabezado Content-Type no incluye ningún juego de caracteres, como en este sitio, Faraday devuelve una cadena binaria (ASCII-8BIT). Nokogiri sigue leyendo el meta-juego de caracteres de la página, pero tus propias comprobaciones de cadenas no lo hacen. Llamar a include?("£") sobre el cuerpo binario provoca Encoding::CompatibilityError: incompatible character encodings: BINARY (ASCII-8BIT) and UTF-8. Etiquetar el cuerpo con force_encoding soluciona ese problema. La documentación de Faraday trata con mayor profundidad los adaptadores, el middleware y las opciones de conexión.

Cómo analizar HTML con Nokogiri

Nokogiri convierte una cadena HTML en un documento que puedes buscar con selectores CSS o XPath. css devuelve todas las coincidencias, at_css devuelve la primera, [] lee un atributo y text lee el contenido.

En books.toscrape.com, cada libro se encuentra en una article.product_pod ficha. Prueba el selector en irb antes de crear nada con él:

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

Hay veinte tarjetas que coinciden, una por libro. La misma consulta en XPath es doc.xpath("//article[@class=\"product_pod\"]"). El CSS es más breve para el marcado basado en clases, mientras que XPath puede buscar coincidencias en el texto y recorrer el árbol. Si tienes que elegir entre selectores CSS o XPath, nuestra comparación analiza las ventajas e inconvenientes, y el tutorial de búsqueda de Nokogiri trata las consultas mixtas. Probar en irb primero significa que un selector erróneo solo cuesta unos segundos, en lugar de una ejecución de 50 páginas que escribe un archivo vacío.

Cómo extraer datos estructurados a hashes de Ruby

Convierte cada ficha en un hash con valores tipados: un título de tipo String, un precio de tipo Float, una valoración de tipo Integer, un indicador de disponibilidad de tipo booleano y una URL absoluta. Los valores tipados se ordenan, filtran y exportan de forma clara. Añade parse_books al módulo:

  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

Por qué está construido de esta manera:

  • El título procede del atributo title , ya que el texto visible se trunca en el caso de los títulos largos.
  • delete("^0-9.") solo conserva los dígitos y el punto decimal, por lo que £51.77 se convierte en 51.77 antes de to_f.
  • La calificación se encuentra en un nombre de clase como star-rating Three, que el RATINGS hash asigna a un entero.
  • URI.join resuelve los enlaces relativos en función de la URL de la página, por lo que cada fila contiene una URL completa.

Pruébalo en la primera página con 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 última línea utiliza next_page_url, que añadirás en la siguiente sección. Una vez implementado, el script muestra:

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

Cómo gestionar la paginación en Ruby

Para paginar, lee el enlace «siguiente» de cada página, resuélvelo en una URL completa y sigue adelante hasta que el enlace desaparezca. Añade un retraso entre páginas y un límite de páginas, para que un sitio con bucle no mantenga tu rastreador en funcionamiento indefinidamente.

El catálogo marca la página siguiente con li.next a. Añade estos dos métodos:

  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

En la última página no hay ningún enlace, por lo que el operador de navegación segura (&.) devuelve nil y el bucle termina. El max_pages: 100 límite te protege de los bucles, y el medio segundo sleep protege el sitio web de picos de tráfico. En este catálogo, crawl se visitan 50 páginas y se obtienen 1.000 libros, que la sección de exportación guarda en archivos.

Seguir los enlaces de una página a otra te lleva del «scraping» al «crawling». Nuestra guía sobre «scraping» frente a «crawling» explica dónde está la línea divisoria.

Cómo extraer datos de varias páginas a la vez en Ruby

Cuando ya conoces las URL de las páginas, un pequeño grupo de subprocesos recurre varias páginas a la vez. Los subprocesos de Ruby resultan útiles en este caso porque el intérprete libera su bloqueo global mientras un subproceso espera en la red. Un tamaño fijo del grupo mantiene predecible la carga en el sitio web.

Las URL de las páginas del catálogo siguen un patrón, así que crea la lista por adelantado y añádela crawl_concurrently al módulo:

  # 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

Cómo encajan todas las piezas:

  • Un Queue cierra los trabajadores de forma ordenada. Tras close, pop devuelve las URL restantes y, a continuación, nil, lo que pone fin a cada bucle, sin necesidad de valores centinela. Consulta la documentación de Thread::Queue.
  • Cada hilo establece su propia conexión, por lo que no se comparte el estado de la conexión entre hilos.
  • Los errores se detectan por página. El rescue registra la URL que ha fallado y sigue adelante, de modo que una página defectuosa no detiene el lote.
  • Queue es seguro para subprocesos, por lo que los trabajadores envían los resultados sin necesidad de un mutex.

Ejecútalo en las 50 páginas más una URL que no existe, utilizando 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}"

La salida muestra todos los libros y la única página que ha fallado:

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

Los resultados llegan en el orden en que los subprocesos terminan, así que ordénalos si el orden es importante. Mantén workers pequeño en sitios web que no sean de tu propiedad: el grupo existe para limitar la carga.

Cómo reintentar las solicitudes fallidas de forma «cortés»

Los valores predeterminados de `faraday-retry` solo reintentan los tiempos de espera agotados y los fallos de conexión, sin espera entre intentos y sin tener en cuenta los códigos de estado HTTP. Un 429 «Demasiadas solicitudes» o un 503 «Servicio no disponible» fallan en el primer intento a menos que configures tú mismo el middleware.

Estas opciones del client método hacen que los reintentos funcionen:

  • retry_statuses: [429, 500, 502, 503, 504] indica al middleware qué respuestas debe reintentar. Sin ello, no se reintenta ninguna.
  • max: 3 permite tres reintentos tras el primer intento.
  • interval: 0.5, backoff_factor: 2, y interval_randomness: 0.5 comienza con una espera de medio segundo, la duplica cada vez y añade variación.
  • max_interval: 30 limita cualquier espera individual a 30 segundos.

Cuando una respuesta reintentada incluye un Retry-After encabezado, el middleware espera tanto tiempo como pida el servidor. Si esa espera es más larga que max_interval, desiste de inmediato: un servidor que responde Retry-After: 120 recibe un Faraday::TooManyRequestsError de este cliente. Genera una excepción max_interval si prefieres esperar. Solo se reintentan los métodos idempotentes, como GET, y un 404 nunca se reintenta.

El orden de las dos líneas de middleware también es importante. Registra f.response :raise_error antes que f.request :retry. Faraday ejecuta el middleware registrado en primer lugar en el exterior, por lo que retry ve la respuesta sin procesar 429 o 503, vuelve a intentarlo y raise_error solo se activa una vez que se agotan los reintentos. Si intercambias las dos líneas, raise_error convierte el 429 en un Faraday::TooManyRequestsError antes de retry vea nunca un código de estado, por lo que no se reintenta nada, ni siquiera con retry_statuses esté configurado.

Con esta configuración, un servidor que devuelva un 429 dos veces y luego un 200 proporcionará a tu código un 200 normal. Un servidor que siga devolviendo un 503 recibirá cuatro solicitudes, tras lo cual tu código recibirá Faraday::ServerError. El archivo README de faraday-retry enumera todas las opciones, incluidas retry_block para registrar los intentos.

Cómo guardar los datos extraídos en CSV y JSON

Escribe el CSV con una lista de encabezados explícita y el JSON con JSON.pretty_generate. Ambos conservan los valores tipificados de la etapa de análisis sintáctico.

Añade los métodos de exportación:

  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 extrae los campos en HEADERS orden, de modo que las filas se alineen con el encabezado aunque añadas claves más adelante. Vincula el rastreo y las exportaciones en 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}"

Crea la carpeta de salida y ejecútalo:

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

El script muestra pages: 50, books: 1000. output/books.csv tiene una fila de encabezado más 1.000 filas de datos, y empieza así:

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 contiene los mismos 1.000 registros como una matriz de objetos. Las notas de la versión de Ruby 3.4 incluyen csv entre las gemas que pasaron a formar parte del paquete, por lo que este es el paso en el que aparece una línea que falta en el Gemfile. Cierra el módulo con un último end después del último método.

Cómo probar un scraper de Ruby sin acceder al sitio web

Guarda una página real como fixture, simula la solicitud con WebMock y comprueba la salida analizada. Estas pruebas se ejecutan sin conexión y detectan desviaciones en los selectores antes de que una tarea programada envíe un CSV vacío.

Guarda la primera página del catálogo:

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

A continuación, escribe 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

Ejecuta las pruebas:

bundle exec ruby test/book_scraper_test.rb

La línea de resumen dice 3 runs, 6 assertions, 0 failures, 0 errors, 0 skips. Las dos primeras pruebas fijan el analizador y la paginación. La tercera simula un 503 seguido de un 200 y comprueba que el cliente devuelva el 200, por lo que falla si alguien cambia el orden del middleware o omite retry_statuses. WebMock también bloquea las llamadas de red reales, por lo que la ausencia de un simulador se muestra como un error claro.

Cómo extraer datos de páginas renderizadas con JavaScript en Ruby

Faraday y Nokogiri solo ven el HTML que envía el servidor. Si la página genera su contenido con JavaScript en el navegador, ese contenido no aparece en la respuesta. Compruébalo primero y luego elige la solución más sencilla: datos incrustados, luego un navegador sin interfaz gráfica y, por último, una API gestionada.

Cómo comprobar si una página necesita JavaScript

Carga la página y cuenta los elementos que esperas encontrar. quotes.toscrape.com/js/ muestra 10 citas en el navegador. Cuéntalas en el HTML sin procesar:

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 primera línea de la salida es div.quote in raw HTML: 0: las citas no están en el HTML del servidor. Puedes ver lo mismo en el navegador. «Ver código fuente» muestra el HTML del servidor, mientras que el panel «Elementos» de DevTools muestra la página tras la ejecución de JavaScript.

Extrae primero el JSON incrustado

Muchas páginas con JavaScript envían sus datos en un script en línea y los representan en el navegador. Leer esos datos directamente es más ligero y fiable que ejecutar un navegador. La segunda mitad del script anterior hace precisamente eso: la expresión regular captura la matriz asignada a var data, y JSON.parse lo convierte en hashes de Ruby.

A continuación, el script muestra:

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"
  ]
}

Son las 10 citas completas, con etiquetas, y sin necesidad de un navegador. La misma idea se aplica cuando una página carga sus datos desde una API: busca la respuesta JSON en la pestaña «Red» de DevTools y solicita esa URL con Faraday.

Representa la página con Ferrum

Cuando los datos solo estén disponibles tras la ejecución de JavaScript, renderiza la página en Chrome sin interfaz gráfica. Ferrum controla Chrome directamente a través del protocolo de DevTools, por lo que no hay que instalar ni mantener sincronizado ningún binario de controlador. Requiere tener Chrome o Chromium en tu PATH, o una ruta en la BROWSER_PATH variable de entorno.

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

Guárdalo como scripts/ferrum_quotes.rb y ejecútalo con bundle exec ruby scripts/ferrum_quotes.rb. Muestra rendered div.quote: 10 seguido de la primera comilla. wait_for_selector se detiene hasta que aparecen las comillas, lo que es mejor que un sleep. page.body devuelve el HTML generado, por lo que el código de Nokogiri es el mismo que para una página estática, y ensure cierra Chrome incluso cuando algo falla.

Cada instancia del navegador consume memoria real y CPU, así que mantén baja la concurrencia de navegadores y renderiza solo las páginas que lo necesiten. La página del proyecto Ferrum documenta clics, escritura y capturas de pantalla.

Selenium, Capybara y otras opciones de navegador

selenium-webdriver funciona a través de WebDriver, por lo que es la opción ideal cuando necesitas Firefox o código compatible con varios navegadores, y Capybara añade un lenguaje de dominio específico (DSL) más intuitivo para hacer clics y rellenar campos. Cuprite es un controlador de Capybara basado en Ferrum, útil si ya escribes código en Capybara. Kimurai integra navegadores y Nokogiri en un marco completo, que se describe más adelante. Si necesitas renderizar, realizar esperas y tomar capturas de pantalla, y prefieres renderizar páginas sin ejecutar navegadores en tus propios servidores, un navegador alojado te libera de esa tarea.

Cómo enviar formularios y mantener sesiones con Mechanize

Mechanize funciona como un navegador sencillo sin JavaScript. Almacena cookies, sigue redireccionamientos y rellena y envía formularios, lo que resulta adecuado para inicios de sesión y flujos de varios pasos en sitios web renderizados por el servidor.

El sitio de prácticas tiene un formulario de inicio de sesión en quotes.toscrape.com/login que acepta cualquier nombre de usuario y contraseña. Añade gem "mechanize", "~> 2.14" a tu Gemfile, ejecuta bundle instally inicia sesión:

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}"

El script muestra logged in: true y quotes on page: 10. form_with localiza el formulario por su acción; los campos ocultos, como el token CSRF, se envían junto con él, y la cookie de sesión permanece en el almacén de cookies del agente para futuras solicitudes. search ejecuta Nokogiri, por lo que funcionan los mismos selectores. Para los inicios de sesión que dependen de JavaScript, utiliza Ferrum en su lugar. Automatiza los inicios de sesión únicamente en sitios web en los que tengas una cuenta y permiso.

¿Deberías utilizar un marco de trabajo de web scraping en Ruby?

La mayoría de los trabajos no lo necesitan. Faraday, Nokogiri y un grupo de subprocesos se encargan de unos cuantos sitios web con lógica personalizada, y tú mantienes el control total. Un marco de trabajo resulta útil cuando ejecutas muchas arañas y quieres disponer de programación, motores de navegador y flujos de trabajo en un solo lugar.

Kimurai es la principal opción en Ruby. La versión 2.x vuelve a estar en mantenimiento y requiere Ruby 3.2 o posterior. Antes de adoptarla, ten en cuenta dos cosas:

  • Incluye un amplio árbol de dependencias, entre las que se encuentran Capybara, Selenium y Active Support.
  • Muchos tutoriales siguen estando orientados a Kimurai 1.x, así que comprueba que los ejemplos se ajusten a la API de la versión 2.x.

Faraday frente a HTTParty

Ambos clientes funcionan. HTTParty es la forma más rápida de realizar una solicitud puntual y analiza las respuestas JSON para convertirlas en hashes de 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}"

Esto muestra status 200, books 20. Faraday requiere más configuración, pero su middleware gestiona los reintentos, los tiempos de espera y el manejo de errores en una única conexión reutilizable. Utiliza HTTParty para scripts rápidos y Faraday para rastreadores que se ejecutan de forma programada.

¿Cuándo se topa con un obstáculo el scraping con Ruby «hazlo tú mismo»?

El rastreador de esta guía funciona porque books.toscrape.com y quotes.toscrape.com son sitios de prueba creados para solicitudes automatizadas. Muchos sitios web de producción no lo están. El mismo código puede empezar a fallar con respuestas 403, páginas de CAPTCHA, límites de frecuencia por IP, comprobaciones de huella digital del navegador o contenido que varía según el país. Aumentar el número de subprocesos no sirve de nada, ya que estos límites dependen de quién realiza la solicitud, no de la velocidad.

Síntoma

Causa probable

Solución casera

Cuándo detenerse

403 Prohibido en todas las solicitudes

El sitio web ha marcado a tu cliente o tu dirección IP

Envía encabezados realistas, reduce la velocidad

El bloqueo persiste incluso con tasas bajas

429 Demasiadas solicitudes

Límite de velocidad por IP o por sesión

Menos trabajadores, respeta el límite Retry-After

Necesitas más ancho de banda del que permite una sola IP

Una página de CAPTCHA en lugar de contenido

Detección de bots

Reduce la frecuencia de las solicitudes y consulta las condiciones de uso del sitio

Aparecen CAPTCHAs en el tráfico normal

Funciona en un navegador, pero falla en Chrome sin interfaz gráfica

Comprobaciones de huella digital

Revisa las opciones de ejecución sin interfaz gráfica

Estás manteniendo los parches del navegador

Contenido o precios diferentes según la región

Respuestas geolocalizadas

Redirigir a través de una IP de ese país

Necesitas muchos países

Cada solución «hazlo tú mismo» añade infraestructura que hay que gestionar y supervisar: grupos de proxies, flotas de navegadores, colas de reintentos. Nuestra guía sobre cómo evitar que te bloqueen aborda los encabezados, el control de ritmo y los proxies. Llega un momento en el que tiene más sentido delegar la obtención de datos y reservar tu código Ruby para el análisis.

Cómo llamar a la API de Scraper desde Ruby

La API de Scraper se encarga de recuperar la página por ti, gestiona los proxies y los reintentos, y puede ejecutar JavaScript, mientras que tu código Ruby mantiene los mismos pasos de Faraday y Nokogiri. Envías una solicitud GET con tu clave de API y la URL de destino, y añades opciones como render_js o country solo cuando una página las necesite.

Añade este método al BookScraper módulo, antes del 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

A continuación, llámalo desde scripts/scraper_api.rb y comprueba el estado antes de analizarla:

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

Establece tu clave en el shell y ejecútalo:

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

¿Por qué el código tiene este aspecto?

  • El hash «params» codifica la URL de destino por ti, por lo que su :, /, ?, y & los caracteres no pueden interrumpir la solicitud. Pegar la URL en la cadena de consulta a mano es un error habitual.
  • ENV.fetch detecta el error rápidamente. Si la variable no está definida, Ruby lanza una excepción KeyError: key not found: "WSA_API_KEY" antes de que se envíe ninguna solicitud.
  • El tiempo de espera es mayor, ya que la representación tarda más que una simple recuperación.
  • Esta conexión no tiene middleware de reintentos. La API ya realiza reintentos por su parte, por lo que los reintentos del cliente solo repetirían la tarea.
  • La rama de error muestra el cuerpo. Con una clave no válida, el script muestra Scraper API returned 401: {"error":"Invalid API key"}.

Con HTTParty, la misma llamada pasa las opciones como query:, lo que las codifica de la misma manera. Mantén la clave de la API en el lado del servidor: no la guardes en el control de versiones, no la incluyas en el código del cliente ni la muestres en los registros.

Errores comunes al extraer datos con Ruby y cómo solucionarlos

La mayoría de los errores en el scraping web con Ruby se deben a la falta de una gem, a una configuración de reintentos que no funciona, a una discrepancia de codificación o a contenido que no se encuentra en el HTML. A continuación se detallan cada uno de los síntomas y su solución.

Error o síntoma

Causa

Solución

cannot load such file -- csv (LoadError)

csv es una gema incluida de serie desde Ruby 3.4

Añade gem "csv" al Gemfile

Las respuestas 429 o 503 fallan en el primer intento

Los valores predeterminados de faraday-retry no incluyen códigos de estado

Configurar retry_statuses

Faraday::TooManyRequestsError a pesar de haber registrado una configuración de reintentos

raise_error registrada después de retry

Registrar raise_error primero

El reintento se da por perdido inmediatamente ante un error 429

Retry-After es más largo que max_interval

Subir max_interval o ralentizar

Encoding::CompatibilityError en las comprobaciones de cadenas

Cuerpo binario (sin juego de caracteres en Content-Type)

force_encoding("UTF-8")

Un selector no devuelve nada

Contenido generado por JavaScript o un selector incorrecto

Comprueba el HTML sin procesar y, a continuación, consulta la sección de JavaScript

Faraday::ResourceNotFound para una página de un lote

Un error 404 generado por raise_error

Rescue Faraday::Error por página

KeyError: key not found: "WSA_API_KEY"

La variable de entorno no está configurada

Exporta esta información en tu shell o gestor de secretos

URL relativas en tu salida

Sin formato href valores

Resuélvelas con URI.join

403 Prohibido

Bloqueo

Reduce la velocidad y, a continuación, consulta la sección «Hazlo tú mismo»

Si un error no aparece en la tabla, muestra el código de estado y los primeros cientos de bytes del cuerpo de la respuesta. La mayoría de los errores de scraping se hacen evidentes en cuanto ves lo que ha enviado el servidor.

Cómo realizar scraping de forma responsable con Ruby

Lee el archivo robots.txt del sitio, que sigue el Protocolo de Exclusión de Robots (RFC 9309), y omite las rutas que no permite para tu agente de usuario. Mantén reducido el grupo de trabajadores, añade retrasos y respeta Retry-After cuando el servidor te pida que reduzcas la velocidad. Identifica a tu cliente con un User-Agent que incluya una dirección de contacto. Sigue las condiciones de uso del sitio y evita recopilar datos personales a menos que tengas una base legal clara para ello. Para saber cómo abordamos esto, consulta nuestras prácticas responsables de datos web.

FAQ

¿Es Ruby adecuado para el web scraping?

Sí. Ruby cuenta con gemas consolidadas para cada paso: Faraday o HTTParty para recuperar páginas, Nokogiri para analizar HTML con selectores CSS o XPath, Ferrum para controlar Chrome sin interfaz gráfica y Mechanize para los formularios. Python cuenta con un ecosistema de scraping más amplio y más herramientas para navegadores, pero Ruby es la elección natural cuando tu aplicación o tu equipo ya utilizan Ruby o Rails.

¿Cuál es la mejor biblioteca de Ruby para el web scraping?

La mayoría de los scrapers de Ruby combinan Faraday para las peticiones HTTP con Nokogiri para el análisis. Añade faraday-retry para los reintentos, la gema csv para hojas de cálculo y la biblioteca JSON integrada para archivos JSON. Utiliza Ferrum cuando una página necesite un navegador real, Mechanize para formularios e inicios de sesión, y Kimurai si quieres un marco completo de rastreo.

¿Debería usar Faraday o HTTParty para el web scraping?

Ambos funcionan. HTTParty es la forma más rápida de realizar una solicitud puntual y analiza las respuestas JSON por ti. Faraday requiere un poco más de configuración, pero establece una conexión reutilizable con middleware, por lo que los reintentos, los tiempos de espera y la gestión de errores se centralizan en un solo lugar. Para un rastreador que se ejecuta de forma programada, el middleware de Faraday suele merecer la pena.

¿Puede Nokogiri extraer datos de páginas renderizadas con JavaScript?

No. Nokogiri analiza el HTML que envía el servidor y nunca ejecuta JavaScript, por lo que el contenido generado en el navegador no aparece en la respuesta. Comprueba primero el HTML sin procesar y, a continuación, busca los datos en un script en línea o en una solicitud JSON que realice la página. Si solo existe tras el renderizado, utiliza Ferrum o Selenium para cargar la página.

¿Se puede realizar scraping web con Ruby on Rails?

Sí. Coloca el rastreador en una clase Ruby sencilla o en un objeto de servicio y, a continuación, ejecútalo desde una tarea en segundo plano con Active Job para que las solicitudes web nunca tengan que esperar a que finalice el rastreo. Almacena los resultados con Active Record, programa ejecuciones periódicas con tu backend de tareas y mantén el código HTTP y de análisis comprobable fuera de Rails con fixtures HTML guardados.

¿Sigue siendo Kimurai un buen marco de trabajo de web scraping para Ruby?

Kimurai 2.x vuelve a recibir mantenimiento y requiere Ruby 3.2 o una versión más reciente, por lo que es una opción cuando se desean arañas, motores de navegador y flujos de trabajo en un único marco. Incorpora un amplio árbol de dependencias, que incluye Capybara y Selenium. Para trabajos más pequeños, Faraday, Nokogiri y un grupo de subprocesos te ofrecen los mismos resultados con menos componentes.

¿Cómo evito que me bloqueen al realizar scraping con Ruby?

Identifica a tu cliente con un User-Agent claro, mantén baja la concurrencia, añade retrasos entre páginas y vuelve a intentar las respuestas 429 y 503 respetando el Retry-After. Respeta el archivo robots.txt y las condiciones de uso del sitio. Los sitios con CAPTCHAs, identificación de huellas digitales o límites de frecuencia por IP pueden bloquear incluso un rastreador que se comporte correctamente; ahí es donde resulta útil rotar las direcciones IP o utilizar una API de rastreo gestionada.

Próximos pasos

Ahora dispones de un scraper en Ruby probado que recupera, analiza, pagina, ejecuta páginas de forma simultánea, vuelve a intentarlo de forma educada y exporta datos limpios. Unos cuantos pasos más lo dejarán listo para producción:

  • Ejecútalo como una tarea en segundo plano. En Rails, envuelve el rastreo en un Active Job y prográmalo, para que ninguna solicitud web tenga que esperar a que finalice un rastreo.
  • Añade un límite de frecuencia por host, de modo que cada trabajador comparta un presupuesto de solicitudes en lugar de depender únicamente de los tiempos de espera y del tamaño del pool.
  • Renderiza solo donde sea necesario. Mantén Faraday para las páginas estáticas y el JSON incrustado, y envía a Ferrum únicamente las páginas que necesiten JavaScript.
  • Delega la recuperación de datos cuando empiecen los bloques. En cuanto dediques más tiempo a los proxies y los CAPTCHAs que al análisis, traslada el paso de recuperación a una API gestionada y conserva el resto de tu código Ruby.

Conserva tu código Ruby, omite los bloques

Dirige el mismo cliente de Faraday a la API de Scraper y obtén la página renderizada, desde el país que elijas, sin tener que ejecutar navegadores ni proxies tú mismo. Empieza con 5.000 solicitudes gratuitas. No se cobran créditos adicionales por el renderizado de JavaScript ni por la geolocalización, y las solicitudes fallidas no se facturan.

Consulta los precios de la API de Scraper

Acerca del autor

Raluca Penciuc, Desarrollador full-stack @ WebScrapingAPI

Raluca Penciuc

Desarrollador full-stack

Raluca Penciuc es desarrolladora full stack en WebScrapingAPI, donde se dedica a crear rastreadores, mejorar las técnicas de evasión y buscar formas fiables de reducir la detección en los sitios web de destino.

Extracción de datos web con AWS Lambda: guía para Python y Java 2026
Guías

Extracción de datos web con AWS Lambda: guía para Python y Java 2026

TL;DR: En resumen: el web scraping con AWS Lambda funciona mejor cuando cada invocación es breve, está delimitada y se puede reintentar de forma independiente. Empieza con HTTP directo, AWS SAM y S3, y añade SQS, contenedores, renderización en navegador, proxies o una capa de recuperación gestionada solo cuando la carga de trabajo demuestre que los necesita.

Suciu Dan33 min read
Leer artículo
Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos
Guías

Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos

TL;DR: En resumen: GoSpider es un rastreador de línea de comandos diseñado para descubrir URL, no un extractor completo de datos estructurados. Esta guía sobre cómo utilizar GoSpider muestra cómo realizar un rastreo limitado, gestionar correctamente los resultados, pasar los datos de Colly a CSV y seguir una ruta de diagnóstico para respuestas 403 o páginas que requieren renderización con JavaScript.

Suciu Dan24 min read
Leer artículo
Cómo raspar Redfin: Guía Python de Datos Inmobiliarios
Guías

Cómo raspar Redfin: Guía Python de Datos Inmobiliarios

TL;DR: Redfin expone puntos finales de API ocultos que devuelven JSON estructurado para los listados de propiedades, lo que permite omitir por completo el frágil análisis HTML. Esta guía te guía a través de la construcción de un raspador de Python que extrae datos de alquiler y venta, busca por ubicación, supervisa los nuevos listados a través de mapas de sitio XML y exporta resultados limpios a CSV o JSON.

Suciu Dan14 min read
Leer artículo

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.