Scraping

Cómo gestionar el scroll infinito y la paginación dinámica al hacer scraping

¿No puedes replicar el endpoint oculto? Gestiona el scroll infinito dentro del render con cadenas de scroll, clics en cargar más y comprobaciones para listas virtualizadas.

Chris Collins

Chris Collins

15 de septiembre de 2026 · 11 min de lectura

La mejor forma de hacer scraping de un feed de scroll infinito suele ser no hacer scroll en absoluto. Detrás de la mayoría de los feeds hay una solicitud paginada, a menudo basada en cursor, y repetir esa solicitud directamente es más rápido, más económico y más fiable que manejar un navegador. Ese enfoque se trata en detalle en cómo hacer scraping de paginación y scroll infinito de forma fiable.

Esta guía es para los casos en los que eso no funciona. La solicitud está firmada con un token de corta duración. La respuesta es un bloque opaco que la página decodifica en el cliente. El feed solo se carga cuando un elemento entra en el viewport. El botón “siguiente página” está conectado a un estado que no puedes reconstruir. En esos casos tienes que gestionar el scroll y los clics dentro de un render real, y hay varias formas en las que esto puede salir mal.

Los ejemplos usan la Shifter Web Scraping API, que ejecuta la página en Chrome headless y acepta una cadena de acciones de navegador antes de la captura.

Cuatro tipos de paginación dinámica

Identifica con cuál te enfrentas antes de escribir nada, porque cada uno necesita una técnica distinta.

PatrónQué dispara el siguiente loteTécnica
Feed activado por scrollUn elemento cerca del final entra en el viewportHacer scroll hasta un centinela, esperar, repetir
Botón “cargar más”Un clic en un botón que añade elementosClic, esperar los nuevos elementos, repetir
Paginación con estado en la URLLa página actualiza la URL a medida que avanzas por los resultadosObtener esas URLs directamente, sin scroll
Lista virtualizadaHay scroll, pero las filas antiguas se eliminan del DOM a medida que aparecen nuevasCapturar en ventanas, o usar la solicitud subyacente

El tercero merece comprobarse primero porque es el más económico de manejar. Haz scroll en un feed en un navegador normal y observa la barra de direcciones. Si cambia un parámetro de página u offset, el sitio ya te ha dado una URL paginada, y cada página es una solicitud ordinaria.

El cuarto es el que pierde datos de forma silenciosa, y tiene su propia sección más abajo.

Renderizar y esperar correctamente

Toda técnica dinámica empieza con los mismos dos controles. render_js=1 ejecuta la página en Chrome headless, al mismo coste de un crédito por solicitud exitosa que una obtención estática. wait_for_css retiene la captura hasta que un selector exista en el DOM, de modo que nunca extraigas de una página que aún no se ha poblado. Los controles de renderizado están documentados en rendering JavaScript.

Elige el selector de espera con cuidado. Esperar al contenedor de la lista no es suficiente, porque muchos frameworks renderizan primero un contenedor vacío y lo rellenan después. Espera en su lugar al primer elemento de la lista.

Feeds activados por scroll: hacer scroll hasta un centinela

Las acciones de navegador van en js_instructions, un array JSON de pasos ejecutados en orden antes de la captura. Las acciones documentadas son scrollTo, click y wait.

El patrón para un feed activado por scroll es hacer scroll hasta un elemento que se encuentra después de la lista, esperar a que cargue el siguiente lote, y repetir. Como la lista crece, ese elemento se desplaza más abajo cada vez, así que volver a hacer scroll hasta él dispara la siguiente carga.

import json
import os

import requests

API = "https://scrape.shifter.io/v1"

instructions = [
    {"action": "click", "selector": "button.accept-cookies"},
    {"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
    {"action": "wait", "duration": 2000},
    {"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
    {"action": "wait", "duration": 2000},
    {"action": "scrollTo", "selector": "footer", "timeout": 5000, "block": "start"},
    {"action": "wait", "duration": 2000},
]

rules = {
    "items": {
        "selector": "article.card",
        "type": "list",
        "item": {
            "link": {"selector": "a.card-link", "output": "@href"},
            "name": {"selector": "h3", "output": "text"},
            "price": {"selector": ".price", "output": "text"},
        },
    }
}

params = {
    "api_key": os.environ["SHIFTER_API_KEY"],
    "url": "https://shop.example.com/category/shoes",
    "render_js": 1,
    "wait_for_css": "article.card",
    "js_instructions": json.dumps(instructions),
    "extract_rules": json.dumps(rules),
}

resp = requests.get(API, params=params, timeout=120)
resp.raise_for_status()
items = resp.json()["items"]

Algunos detalles ahí son deliberados.

El banner de cookies se descarta primero, porque una superposición puede interceptar el scroll o el clic. La espera entre desplazamientos da tiempo a que la solicitud de red se complete y el DOM se actualice; demasiado corta y capturas antes de que llegue el lote. extract_rules con un tipo lista devuelve cada tarjeta como un objeto JSON, así que no hay parser en tu lado, y requests codifica en URL ambos parámetros JSON por ti. La sintaxis de extracción está en extraction rules.

Prueba la cadena en una página de muestra antes de ejecutarla a escala, y ajusta el número de pasos de scroll y la duración de la espera según lo rápido que ese sitio realmente cargue.

Botones “cargar más”: clic, luego esperar el crecimiento

Un botón “cargar más” es el mismo bucle con un disparador distinto:

[
  {"action": "click", "selector": "button.load-more", "timeout": 3000},
  {"action": "wait", "duration": 2000},
  {"action": "click", "selector": "button.load-more", "timeout": 3000},
  {"action": "wait", "duration": 2000}
]

Dos cosas suelen sorprender a la gente. El selector del botón a menudo cambia de estado mientras carga, ganando una clase disabled o un spinner, así que el segundo clic puede dispararse antes de que el botón vuelva a ser clicable, a menos que la espera sea lo bastante larga. Y el botón normalmente desaparece cuando no queda nada más que cargar, lo cual es una señal útil de finalización pero significa que tu cadena necesita tolerar que los últimos clics no tengan nada sobre lo que actuar. De nuevo, prueba contra el sitio real.

El presupuesto de tiempo de un único render

Todo lo anterior ocurre dentro de una sola sesión de navegador con un límite de tiempo. wait_for_css expira a los 30 segundos por defecto, y timeout limita cuánto tiempo puede pasar el navegador en la página. Un feed con miles de elementos no terminará de cargar dentro de un único render, sin importar cuántos pasos de scroll encadenes.

Así que divide el problema en lugar de alargar la cadena.

Reduce el conjunto de resultados. Los filtros, los órdenes de clasificación y las facetas de categoría suelen producir feeds más pequeños. Veinte feeds acotados que cargan por completo cada uno son más fiables que un feed enorme que nunca termina.

Pagina a través de URLs filtradas. Muchos sitios combinan un filtro con estado en la URL, lo que convierte un scroll ilimitado en un conjunto finito de solicitudes ordinarias.

Recurre a la solicitud subyacente cuando el feed sea genuinamente largo y no se pueda acotar. Obtenlo sin renderizar; para endpoints que devuelven JSON, auto_parser=1 devuelve el cuerpo ya parseado.

Listas virtualizadas: la pérdida silenciosa de datos

Algunos feeds, en particular los muy largos, usan virtualización de listas. Solo existen en el DOM las filas cercanas al viewport. A medida que haces scroll hacia abajo, las filas de arriba se eliminan.

Si haces scroll de una lista virtualizada hasta el final y capturas, obtienes la última pantalla de elementos, no todos los elementos por los que pasaste al hacer scroll. No aparece ningún error. La extracción devuelve una lista limpia, plausible e incompleta.

Detéctalo antes de confiar en cualquier salida. Ejecuta la cadena de scroll, y luego comprueba si los elementos de la primera pantalla siguen presentes en el resultado capturado. Si han desaparecido, la lista está virtualizada, y hacer scroll y luego capturar no puede funcionar. Usa en su lugar paginación con estado en la URL o la solicitud subyacente.

Paginación dinámica entre solicitudes

Cuando cada página es una solicitud separada que depende de estado del lado del servidor, como un cursor almacenado contra tu sesión, mantén ese estado constante a lo largo del recorrido con session_id. Una sesión persiste las cookies, el estado del navegador y la IP upstream a través de las solicitudes, y expira tras 10 minutos de inactividad. Mantén el mismo country durante toda la vida de una sesión, ya que cambiarlo a mitad del recorrido puede invalidar cookies ligadas a la configuración regional. Los detalles están en sessions and proxies.

params.update({
    "session_id": "shoes-walk-07",
    "country": "de",
})

Mantén el recorrido en movimiento. Una sesión que permanece inactiva mientras tu código hace algo lento entre páginas expirará y romperá el cursor.

Saber que lo has capturado todo

Un scraper que se detiene antes de tiempo se ve exactamente igual que un scraper que ha terminado. Incorpora comprobaciones de completitud en cada ejecución.

  • Compara contra el total mostrado. Muchos feeds muestran un recuento de resultados. Si la página dice 1.284 y extrajiste 960, te has detenido antes de tiempo.
  • Deduplica sobre una clave estable, como el enlace o el ID del elemento, nunca sobre la posición. Los feeds de scroll re-renderizan elementos con frecuencia, y la posición cambia cuando se inserta contenido.
  • Comprueba la marca de fin. Un botón “cargar más” que ha desaparecido o un mensaje de fin de resultados es una confirmación positiva. Su ausencia tras tu último paso significa que la cadena fue demasiado corta.
  • Sigue la completitud a lo largo del tiempo. Una categoría que ayer dio 1.200 elementos y hoy da 400 normalmente ha cambiado su markup o su comportamiento de carga, no su inventario.

Créditos y latencia: elegir el enfoque por sitio

EnfoqueCréditosLatenciaRiesgo de fiabilidad
Repetir la solicitud subyacenteNinguno si se envía directamente; uno por página a través de la APIBajaSolicitudes firmadas u opacas
Paginación con estado en la URLUno por páginaBaja a moderadaNecesita una URL paginada
Cadena de scroll o clic en un único renderUno por toda la cadenaAltaPresupuesto de tiempo, virtualización

Un render que hace scroll a través de varios lotes cuesta un crédito, mientras que repetir esos mismos lotes como solicitudes separadas cuesta un crédito cada una. La contrapartida es la latencia y el presupuesto de tiempo: las cadenas largas son más lentas y más propensas a expirar. Usa cadenas de scroll para feeds moderados donde la solicitud subyacente resulte poco práctica, y recurre a las otras dos para todo lo demás.

Preguntas frecuentes

¿Debería renderizar siempre JavaScript para el scroll infinito?

No. Comprueba primero la paginación con estado en la URL y una solicitud subyacente que se pueda repetir. El renderizado es el recurso de respaldo, no la opción por defecto.

¿Cuántos pasos de scroll debe tener la cadena?

Tantos como el sitio necesite para cargar los lotes que quieras dentro del presupuesto de tiempo. Mídelo en una página de muestra, y si necesitas más de lo que el presupuesto permite, acota el feed en su lugar.

¿Por qué mi extracción devuelve menos elementos de los que he recorrido con scroll?

Casi siempre es virtualización de listas, donde las filas salen del DOM a medida que haces scroll. Comprueba si los elementos de la primera pantalla sobreviven hasta la captura.

¿Cada paso de scroll cuesta un crédito?

No. Toda la cadena se ejecuta dentro de una única solicitud, y una solicitud exitosa cuesta un crédito.

La conclusión

El scroll infinito es paginación por cursor con un navegador delante, y cuando puedes llegar directamente al cursor, deberías hacerlo. Cuando no puedes, gestiónalo dentro del render: espera al primer elemento en lugar de al contenedor, haz scroll hasta un centinela o haz clic en “cargar más” con suficiente espera entre pasos, respeta el presupuesto de tiempo acotando el feed, comprueba la virtualización antes de confiar en una captura, y verifica la completitud contra los totales propios del sitio.

Para decidir cuándo una solicitud de API con render es la herramienta adecuada en primer lugar, consulta cuándo necesitas una web scraping API para sitios con mucho JavaScript. El producto está en la página de web scraping API with JS rendering, con planes en la página de precios.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar