Volver al blog

Raspado de desplazamiento infinito: deja de adivinar cuántas veces desplazarte

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

04-Aug-2026

TL;DR:

  • Una página de desplazamiento infinito casi no envía nada en su HTML. La demostración utilizada aquí envía 2,671 bytes que contienen cero elementos de cita; cada elemento llega después.
  • Un conteo fijo de desplazamientos es el fallo que todos implementan. Cinco desplazamientos devolvieron 60 de 100 elementos — sin errores, sin advertencias, una salida limpia con el 40% de los datos faltantes.
  • Desplázate hasta que el conteo de elementos deje de crecer. Eso devolvió todos los 100.
  • El número de desplazamientos no es una propiedad del sitio. El mismo bucle necesitó 11 acciones de desplazamiento localmente y 3 en un navegador en la nube, alcanzando 100 en ambas ocasiones.
  • La página realizó 10 llamadas a /api/quotes?page=N. Leer ese endpoint directamente devuelve los mismos 100 elementos y reporta has_next, por lo que el bucle sabe cuándo ha terminado.
  • El plan gratuito de Scrapeless cubre la ejecución en el navegador en la nube en esta guía.

El desplazamiento infinito rompe a los scrapers en silencio. La solicitud tiene éxito, los selectores coinciden, el script sale cero — y el conjunto de datos es corto. Nada en la ejecución te dice cuánto te has perdido, porque la página nunca prometió cuánto había.

Esta guía mide eso en una página de demostración en vivo. Construye el bucle que la mayoría de los tutoriales envían, muestra exactamente lo que deja caer, lo reemplaza con uno que tiene una condición de parada real, y luego encuentra la solicitud que la página estaba realizando todo el tiempo.

Lo Que El Servidor Realmente Envía

Fetch la página sin un navegador y no hay nada que analizar:

python Copy
def served_html() -> tuple[int, int]:
    request = urllib.request.Request(SCROLL_URL, headers={"User-Agent": UA})
    with urllib.request.urlopen(request, timeout=45) as response:
        html = response.read().decode("utf-8", "replace")
    return len(html), html.count('class="quote"')
text Copy
bytes de la página de desplazamiento : 2671
elementos de cita en el html         : 0

2,671 bytes y no un solo elemento. La marca es un cascarón; el contenido se obtiene mediante script después de que la página se carga, generalmente cuando un elemento centinela cerca de la parte inferior entra en vista — el mecanismo la especificación del Intersection Observer define. Un selector CSS no puede fallar de forma más limpia que esto: no coincide con nada porque aún no hay nada allí.

Instalar

bash Copy
pip install playwright
playwright install chromium

El segundo comando descarga el binario del navegador; el paquete pip solo no se lanzará. La ejecución de verificación utilizó Playwright 1.59.0.

El Bucle Que Todos Implementan

La receta estándar es desplazarse un número fijo de veces y luego leer la página. page.mouse.wheel() despacha un evento real de rueda del tipo la especificación de Eventos UI define, que es lo que el propio oyente de la página está esperando:

python Copy
def scroll_fixed(page, times: int, pause: float) -> int:
    for _ in range(times):
        page.mouse.wheel(0, 20000)
        time.sleep(pause)
    return page.locator("div.quote").count()

Cinco desplazamientos contra la página en vivo:

text Copy
citas después de 5 desplazamientos : 60
segundos transcurridos               : 5.1

Sesenta elementos. La página contiene cien. El script no arrojó nada, el selector funcionó, y la ejecución parece exitosa desde el exterior — lo que hace que esta sea la versión costosa del error. Elige cinco porque funcionó una vez, y cada ejecución posterior hereda una silenciosa falta del 40%.

Aumentar el número tampoco es una solución. Cambia una recolección insuficiente por desplazamientos desperdiciados, y todavía codifica una conjetura sobre un sitio que puede cambiar su tamaño de lote cuando quiera.

Desplázate Hasta Que Deje de Crecer

La condición que realmente deseas es observable: sigue desplazándote mientras el conteo de elementos aún esté aumentando y detente una vez que se mantenga estable.

python Copy
def scroll_until_stable(page, pause: float, no_growth_limit: int = 2) -> tuple[int, int]:
    seen = page.locator("div.quote").count()
    scrolls = 0
    stable = 0
    while stable < no_growth_limit:
        page.mouse.wheel(0, 20000)
        time.sleep(pause)
        scrolls += 1
        count = page.locator("div.quote").count()
        if count == seen:
            stable += 1
        else:
            stable = 0
            seen = count
    return seen, scrolls

no_growth_limit decide cuánta evidencia necesitas antes de creer que la página ha terminado. Una lectura plana no es prueba — un lote aún en vuelo se ve idéntico al final de la lista. Requerir dos lecturas planas consecutivas cuesta un desplazamiento extra y elimina esa ambigüedad.

text Copy
citas recogidas          : 100
acciones de desplazamiento realizadas : 11
segundos transcurridos    : 11.1
llamadas a la api que hizo la página : 10
primer url de la api      : https://quotes.toscrape.com/api/quotes?page=1

Todos los 100, y el bucle descubrió el conteo en lugar de asumirlo. Ten en cuenta que la cifra de tiempo transcurrido es mayormente la pausa que este bucle elige esperar — once desplazamientos a un segundo cada uno. Es una propiedad del bucle, no del sitio.

El Conteo de Desplazamientos No Es una Propiedad del Sitio

Ejecutando la misma función contra un navegador en la nube en lugar de Chromium local:

text Copy
citas recopiladas        : 100
acciones de desplazamiento realizadas : 3

Misma función, misma página, mismos 100 elementos — 3 acciones de desplazamiento en lugar de 11. La altura de la ventana gráfica y la rapidez con que llega cada lote determinan cuánto avanza la página un evento de rueda, y ninguno de estos factores está bajo tu control. Las dimensiones de la ventana gráfica contra las que se mide el desplazamiento son las que especifica el Módulo de Vista CSSOM. Cualquier recuento de desplazamientos codificado de forma rígida está calibrado al tamaño de la ventana de una máquina.

Esa ejecución utilizó el Navegador de Scraping Scrapeless, que Playwright se conecta a través del Protocolo de Herramientas de Desarrollo de Chrome. Solo cambia la línea de conexión:

python Copy
    endpoint = (
        "wss://browser.scrapeless.com/api/v2/browser"
        f"?token={os.environ['SCRAPELESS_API_KEY']}&sessionTTL=180&proxyCountry=ANY"
    )
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(endpoint, timeout=90000)
        page = browser.new_page()
        page.goto(SCROLL_URL, wait_until="domcontentloaded")
        cloud_total, cloud_scrolls = scroll_until_stable(page, pause=1.0)

connect_over_cdp reemplaza chromium.launch() y se comunica a través de el Protocolo de Herramientas de Desarrollo de Chrome a través de un socket en lugar de a un proceso local, así que el bucle de desplazamiento, los selectores y la extracción permanecen exactamente como estaban. Mantén la clave en el entorno como SCRAPELESS_API_KEY; la introducción al Navegador de Scraping documenta los parámetros restantes de la sesión.

Comenzar toma un minuto — crea una cuenta gratuita de Scrapeless y el plan gratuito cubre esta ejecución.

Observa lo que la página está solicitando

El bucle de desplazamiento es una forma de pedir a la página que obtenga datos. Escuchar las solicitudes muestra qué está obteniendo:

python Copy
        calls: list[str] = []
        context = browser.new_context(user_agent=UA)
        context.on("request", lambda r: calls.append(r.url) if "/api/quotes" in r.url else None)

Diez llamadas, siendo la primera https://quotes.toscrape.com/api/quotes?page=1. La página está paginando un endpoint JSON y renderizando los resultados; el desplazamiento es solo el desencadenante.

Ese endpoint se puede leer directamente y responde a la pregunta que el bucle de desplazamiento tuvo que inferir:

python Copy
def read_api() -> tuple[int, int]:
    quotes = 0
    page = 1
    while True:
        request = urllib.request.Request(API_URL.format(page=page), headers={"User-Agent": UA})
        with urllib.request.urlopen(request, timeout=45) as response:
            payload = json.loads(response.read().decode())
        quotes += len(payload["quotes"])
        if not payload["has_next"]:
            return quotes, page
        page += 1
text Copy
citas recopiladas        : 100
páginas de api solicitadas: 10
segundos transcurridos   : 3.8

Los mismos 100 elementos, con has_next como una condición de terminación explícita en lugar de "el recuento dejó de cambiar". La respuesta ya está estructurada, por lo que no hay un selector entre tú y los campos — lo que también significa que no hay un selector que se rompa cuando se reestiliza el marcado.

Esto vale la pena revisar antes de escribir cualquier bucle de desplazamiento. No siempre existe: muchos sitios renderizan del lado del servidor al desplazarse, firman sus solicitudes o devuelven fragmentos HTML en lugar de JSON. Cuando sí existe, es el objetivo más duradero, y el navegador sigue siendo lo que te muestra que está allí. Para las otras formas de paginación — botones de siguiente, páginas numeradas, cargar más — la guía completa de paginación cubre cada tipo.

Ejecútalo

bash Copy
export SCRAPELESS_API_KEY="tu-clave-api"
python3 scroll_demo.py

La salida completa de la ejecución de verificación:

text Copy
playwright 1.59.0
--- lo que el servidor realmente envía ---
bytes de página de desplazamiento : 2671
elementos de cita en el html   : 0
--- número fijo de desplazamientos ---
citas después de 5 desplazamientos : 60
segundos transcurridos          : 5.1
--- desplazamiento hasta que el recuento deja de crecer ---
citas recopiladas        : 100
acciones de desplazamiento realizadas : 11
segundos transcurridos   : 11.1
llamadas a la api que realizó la página  : 10
primer url de api            : https://quotes.toscrape.com/api/quotes?page=1
--- el mismo bucle en el Navegador de Scraping ---
citas recopiladas        : 100
acciones de desplazamiento realizadas : 3
--- leyendo la misma api directamente ---
citas recopiladas        : 100
páginas de api solicitadas: 10
segundos transcurridos   : 3.8

La proporción en la última línea compara el reloj de pared del bucle del navegador con las lecturas directas. La mayor parte del lado del navegador es la pausa deliberada de un segundo entre desplazamientos, así que interprétalo como el costo de sondear una página para una condición que nunca informa — no como un punto de referencia del navegador en sí.

Solución de problemas

La cuenta nunca deja de crecer. Algunas páginas repiten su contenido. Limita el bucle con un conteo máximo de desplazamientos junto con la verificación de estabilidad, y trata de alcanzar ese límite como una señal para inspeccionar la página en lugar de como una ejecución completada.

Cero elementos incluso después de desplazarse. El contenedor puede desplazarse en lugar de la ventana. Desplaza el elemento en sí con page.locator(...).hover() seguido de page.mouse.wheel(...), o llama a scroll_into_view_if_needed() en el último elemento.

La cuenta crece, luego la página se queda en blanco. Las listas largas a menudo están virtualizadas, por lo que las filas se eliminan del DOM a medida que salen del campo de visión. Recoge los elementos a medida que avanzas en lugar de leerlos todos al final.

Funciona en modo encabezado y no en modo sin encabezado. El tamaño del viewport difiere entre los dos, lo que cambia la distancia que recorre un evento de rueda y si el cargador entra en vista. Establece un viewport explícito en el contexto.

playwright._impl._errors.Error: El ejecutable no existe. El binario del navegador nunca se descargó. Ejecuta playwright install chromium.

Conclusión

El desplazamiento infinito convierte "¿cubrí todo?" en una pregunta que tu scraper tiene que responder por sí mismo, y un conteo de desplazamientos fijo lo responde adivinando. Las medidas aquí muestran lo que eso cuesta: 60 elementos de 100 de cinco desplazamientos, y un conteo de desplazamientos que cambió de 11 a 3 simplemente al pasar a un navegador diferente.

Bucea en una condición observada — conteo de elementos estable a través de lecturas consecutivas — en lugar de un número que elegiste. Y antes de escribir el bucle, observa las solicitudes de la página: cuando el contenido llega como JSON con una bandera has_next, la página ya te ha dicho cómo saber cuándo has terminado.

¿Listo para intentarlo? Comienza con el plan gratuito de Scrapeless y consulta los precios actuales para volúmenes mayores.

FAQ

P: ¿Cuántas veces debo desplazarme?

No elijas un número. Las medidas anteriores utilizaron un bucle contra dos navegadores y necesitaban 11 desplazamientos en uno y 3 en el otro para los mismos 100 elementos, porque la altura del viewport y el tiempo de lote deciden cuán lejos llega cada evento de rueda. Bucle mientras el conteo de elementos siga creciendo y detente después de que se mantenga estable dos veces.

P: ¿Cómo sé que la página ha terminado de cargar todo?

Dos lecturas consecutivas con un conteo de elementos invariable son la señal práctica, porque una sola lectura plana es indistinguible de un lote que todavía está en vuelo. Si la solicitud subyacente de la página expone una bandera como has_next, esa es una respuesta definitiva en lugar de una inferencia.

P: ¿Necesito un navegador para el desplazamiento infinito en absoluto?

A menudo no. La página de demostración aquí carga su contenido desde un endpoint JSON que puede ser leído directamente para los 100 elementos. El navegador sigue siendo cómo descubres ese endpoint — adjunta un listener de solicitud, desplázate una vez y lee las URL. Los sitios que renderizan del lado del servidor en el desplazamiento o firman sus solicitudes sí necesitan el navegador.

P: ¿Por qué mi scraper devuelve menos elementos de los que muestra la página?

O el bucle se detuvo temprano, o la lista está virtualizada y las filas se eliminaron del DOM a medida que se desplazaron fuera de la vista. Imprime el conteo de elementos después de cada desplazamiento: un conteo que sube y luego baja indica virtualización, y un conteo que aún sube cuando el bucle termina indica que el bucle se detuvo demasiado pronto.

P: ¿wait_until="networkidle" resuelve esto?

No. Espera a que la carga inicial se asiente, lo cual ocurre antes de que cualquier desplazamiento haya provocado una solicitud. Los lotes llegan en respuesta a eventos de desplazamiento, por lo que la página puede estar inactiva y casi vacía al mismo tiempo — lo que es exactamente el estado de 2,671 bytes medido al principio.

En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.

Artículos más populares

Catalogar