Páginas estáticas y dinámicas en web scraping

Publicado:

16 min de lectura

Acar Diveroli
Autor: Acar Diveroli
Un nodo bot entre una ventana estática llena de líneas de código y una ventana dinámica con marcadores de carga

Casi todo el que empieza con el scraping vive el mismo momento: los productos, los precios y las reseñas están delante de ti en el navegador, pero cuando obtienes la misma dirección en Python con requests, los selectores no encuentran nada. Al mirar el HTML de la página, ves un texto «Cargando...» y unas cuantas etiquetas <script> en lugar de una lista de productos. El problema no está en tu código; la página es dinámica, y el JavaScript que se ejecuta en el navegador obtiene el contenido después.

En este artículo explicamos la diferencia entre páginas estáticas y dinámicas, cómo saber en pocos minutos si una página es dinámica y qué es un navegador headless. La parte en la que más nos centramos es cómo llegar a los datos sin navegador headless en la mayoría de los casos: encontrar la petición de API que la página hace en segundo plano y leer el JSON incrustado en el HTML. Cuando un navegador es realmente necesario, mostramos también las estrategias de espera correctas en Playwright y formas de reducir el costo. Ejecutamos los ejemplos contra una página de prueba local que carga su contenido con JavaScript.

¿Qué es una página estática?

En una página estática, todo lo que muestra el navegador está en el HTML de la primera respuesta del servidor. El servidor puede leer ese HTML de un archivo preparado o generarlo desde una base de datos en cada petición; lo que importa para el scraping es que el contenido ya está dentro del HTML cuando llega al navegador.

No necesitas un navegador para obtener datos de una página estática:

python
import requests
from bs4 import BeautifulSoup

html = requests.get("https://example.com/deals", timeout=20).text
cards = BeautifulSoup(html, "html.parser").select("div.product")
print("Tarjetas de producto encontradas:", len(cards))

Los sitios de noticias, los blogs, muchos sitios corporativos y las tiendas online renderizadas en el servidor pertenecen a este grupo.

¿Qué es una página dinámica (cargada con JavaScript)?

En una página dinámica, la primera respuesta del servidor es un esqueleto: el diseño de la página, un contenedor de lista vacío y archivos JavaScript. El contenido llega después en estos pasos:

  1. El navegador recibe el esqueleto HTML y dibuja en pantalla un marcador como «Cargando...».
  2. Se descargan y ejecutan los archivos JavaScript. A menudo un framework como React o Vue.
  3. JavaScript envía una petición de API. Por ejemplo, con fetch a /api/products?category=headphones&page=1.
  4. El servidor devuelve los datos en JSON. Nombres de producto, precios, información de stock.
  5. JavaScript construye HTML a partir de ese JSON y lo inserta en la página. Las tarjetas de producto solo aparecen en este paso.
  6. Hacer scroll o clic lanza nuevas peticiones. Scroll infinito, un botón «Ver más», filtros.

Un cliente HTTP como requests de Python solo hace el primer paso; no ejecuta JavaScript. Por eso el HTML que recibe no tiene tarjetas de producto. En nuestra página de prueba local, el código de requests anterior encontró cero tarjetas; la misma página mostraba dos productos en el navegador.

También existe una estructura híbrida: frameworks como Next.js y Nuxt renderizan en el servidor el primer estado de la página y además incrustan los mismos datos en el HTML como un bloque JSON para que los use JavaScript. En estas páginas los datos están en el HTML sin ejecutar JavaScript, pero a veces dentro de una etiqueta <script> y no en las tarjetas visibles.

¿Cómo saber si una página es dinámica?

Unos minutos de diagnóstico antes de montar un navegador headless ayudan a elegir la vía correcta:

  1. Mira el código fuente. Ctrl + U en la página (Cmd + Option + U en macOS) muestra el HTML sin procesar que envió el servidor. Busca ahí un nombre de producto o un precio que ves en la página. Si lo encuentras, la página probablemente es estática.
  2. Compara con el panel Elements. El panel Elements de las herramientas para desarrolladores muestra la página después de ejecutar JavaScript. Si el texto está en Elements pero no en el código fuente, el contenido llegó por JavaScript.
  3. Desactiva JavaScript y recarga. En las herramientas para desarrolladores de Chrome, abre el menú de comandos con Ctrl + Shift + P, escribe «Disable JavaScript» y recarga la página. Si el contenido desaparece, la página es dinámica.
  4. Compruébalo con código. Busca en el HTML obtenido con requests un texto que esperas. Si no está y el HTML es muy corto, has recibido un esqueleto.
  5. Mira el filtro Fetch/XHR del panel Network. Si al recargar la página ves respuestas JSON en este filtro, los datos probablemente están en una de esas peticiones.

El paso cuatro tiene una trampa: si el contenido no está en el HTML, el motivo no siempre es JavaScript. Puede que el sitio haya devuelto una página de verificación u otro contenido. Revisa el código de estado y el contenido de la respuesta; explicamos ese diagnóstico en detalle en Cómo hacer web scraping sin que te bloqueen.

¿Qué es un navegador headless?

Un navegador headless es un navegador real que funciona sin abrir una ventana en pantalla. El motor de Chromium, Firefox o WebKit carga la página como un navegador normal, ejecuta JavaScript, envía peticiones de API y construye la página; tú lees esa página con código. Las herramientas habituales son Playwright, Puppeteer y Selenium.

Un navegador headless puede hacer más que un cliente HTTP, pero también cuesta más:

  • CPU y memoria. Cada pestaña de navegador usa muchas veces los recursos de una petición HTTP. El número de páginas que puedes ejecutar a la vez en la misma máquina lo limitan los núcleos y la memoria, no la red.
  • Tiempo. Descargar y ejecutar todo el JavaScript de una página puede llevar segundos.
  • Número de peticiones y ancho de banda. Una sola página genera decenas de peticiones adicionales para imágenes, fuentes, hojas de estilo, scripts de analítica y anuncios. Eso aumenta tanto tu tráfico como la carga del sitio de destino.
  • Mantenimiento. Las versiones de navegador, los drivers y la lógica de espera añaden complejidad.

Por eso un navegador headless debería ser el último recurso, no la primera opción. Para comparar herramientas, consulta Web scraping: ¿JavaScript o Python?, y para configurar Selenium, Cómo usar un proxy con Selenium.

Primero, encontrar la petición API/XHR

Los datos de una página dinámica llegan al navegador desde algún sitio en JSON. Si encuentras esa petición, puedes obtener los datos directamente sin renderizar la página. La referencia del panel Network de Chrome explica en detalle los filtros y las opciones de copia; la vía corta es:

  1. Abre las herramientas para desarrolladores (F12) y cambia a la pestaña Network.
  2. Marca Fetch/XHR en la barra de filtros.
  3. Recarga la página o realiza la acción que carga los datos (hacer scroll, elegir un filtro, pasar a la página siguiente).
  4. Haz clic en las peticiones cuyo nombre contenga palabras como api, graphql, search o products y mira el JSON en la pestaña Response.
  5. Cuando encuentres la petición con los datos que necesitas, anota la dirección, los parámetros y las cabeceras necesarias desde la pestaña Headers. Puedes probarla en la línea de comandos con clic derecho en la petición y Copy > Copy as cURL.

Repetir en Python la petición encontrada suele llevar unas pocas líneas:

python
import requests

session = requests.Session()
session.headers.update({
    "User-Agent": "ExamplePriceBot/1.0 (+https://example.com/about-our-bot)",
    "Accept": "application/json",
})

response = session.get(
    "https://example.com/api/products",
    params={"category": "headphones", "page": 1},
    timeout=20,
)
response.raise_for_status()
for product in response.json()["products"]:
    print(product["name"], product["price"])

Las ventajas de esta vía son grandes: los datos llegan estructurados, no necesitas selectores HTML, tu código no se rompe cuando cambia el diseño de la página y una sola petición es una pequeña fracción de la carga de una página entera. La paginación suele hacerse con un parámetro page, offset o cursor.

A qué prestar atención:

  • Autenticación y tokens. Algunas peticiones de API necesitan cookies, tokens de sesión o firmas de corta duración. Copiarlos a mano funciona un rato y luego se rompe.
  • Condiciones del sitio. Una API interna que el sitio usa para su propio frontend no es una API pública. Aplica también a estas peticiones las condiciones de uso y las reglas de robots.txt del sitio; si se ofrece una API oficial, prefiérela. Para el marco legal, consulta ¿Es legal el web scraping?.
  • Velocidad. Como las peticiones de API son ligeras, se pueden enviar muy rápido, y eso también significa que es más fácil superar los límites de un sitio. Aplica las mismas reglas de velocidad.

Leer el JSON incrustado en el HTML

En sitios hechos con Next.js, Nuxt y frameworks similares, los datos suelen estar listos en una etiqueta <script> dentro del HTML. Mira el código fuente y busca __NEXT_DATA__, __NUXT__, __INITIAL_STATE__ o application/ld+json.

python
import json

import requests
from bs4 import BeautifulSoup

html = requests.get("https://example.com/deals", timeout=20).text
soup = BeautifulSoup(html, "html.parser")

data = json.loads(soup.select_one("script#__NEXT_DATA__").string)
for product in data["props"]["pageProps"]["products"]:
    print(product["name"], product["price"])

Los bloques application/ld+json contienen datos estructurados como el nombre del producto, el precio, el stock y la valoración en formato schema.org, y como se preparan para los buscadores, suelen ser estables. Tratamos cómo hacer selectores robustos ante cambios de diseño en Selector CSS o XPath.

Estrategias de espera en Playwright

Si los datos no están ni en una API ni en JSON incrustado, o si llegar al contenido requiere interacciones como hacer clic, scroll y rellenar formularios, se usa un navegador headless. El error más habitual entonces es cuándo leer: si lees nada más abrir la página, el contenido todavía no ha llegado; si esperas un tiempo fijo, o pierdes tiempo o, con una respuesta lenta, obtienes igualmente un resultado vacío.

Las herramientas de espera adecuadas de Playwright son:

  • La espera automática de los locators. Operaciones como page.locator("div.product h2").inner_text() esperan a que el elemento aparezca en la página, hasta el timeout predeterminado. La documentación de comprobaciones de accionabilidad de Playwright enumera qué se espera.
  • Esperar un elemento concreto. page.locator("div.product").first.wait_for() espera a que aparezca la primera tarjeta de producto.
  • Esperar una respuesta concreta. page.expect_response(...) espera a que vuelva la petición de datos de la página y te da el JSON directamente.
  • No usar esperas fijas. page.wait_for_timeout(5000) es útil en pruebas, pero poco fiable en producción.

La documentación de Playwright desaconseja el estado de carga networkidle, es decir, esperar a que el tráfico de red se detenga un tiempo; en páginas cuyos scripts de analítica y anuncios siguen enviando peticiones, ese estado puede no llegar nunca.

El ejemplo siguiente abre la página a través de un proxy, bloquea imágenes y fuentes para reducir la carga, captura la respuesta de la petición de datos y después lee las tarjetas renderizadas:

python
from playwright.sync_api import sync_playwright

PROXY = {"server": "http://pr.proxynet.io:8000", "username": "user", "password": "pass"}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    page = browser.new_page()

    # No descargar imágenes ni fuentes, para ahorrar tiempo y tráfico
    page.route("**/*.{png,jpg,jpeg,webp,gif,woff2}", lambda route: route.abort())

    with page.expect_response(lambda r: "/api/products" in r.url and r.ok) as response:
        page.goto("https://example.com/deals")
    api_data = response.value.json()           # obtener el JSON directamente

    page.locator("div.product").first.wait_for()  # o esperar a las tarjetas renderizadas
    names = page.locator("div.product h2").all_inner_texts()

    print(len(api_data["products"]), names)
    browser.close()

En este ejemplo, el JSON capturado con expect_response suele bastar por sí solo; no hace falta leer las tarjetas. Al final usas el navegador solo para que la página envíe esa petición con los parámetros y las cookies correctos.

En Playwright, los datos del proxy se indican al iniciar el navegador, con el usuario y la contraseña en campos separados. Para flujos en los que hay que mantener la misma IP durante la sesión puede usarse una dirección de salida fija; para muchas páginas independientes, un Proxies rotativos que da una IP distinta en cada conexión.

Costo: ¿cuándo sobra un navegador headless?

EnfoqueCuándo funcionaVelocidad y recursosFragilidadA qué prestar atención
Cliente HTTP + HTMLLa página es estática, el contenido está en el código fuenteMuy rápido, muy ligeroSensible a cambios de diseñoAnclar los selectores a atributos estables
JSON incrustado en el HTMLPáginas híbridas como Next.js, NuxtMuy rápido, ligeroLa estructura de datos puede cambiarGestión de errores que compruebe la ruta del JSON
Petición de API en segundo planoEl contenido llega en JSONMuy rápido, el más ligeroIndependiente del diseño, la API puede cambiarCondiciones del sitio, tokens, límites de velocidad
Navegador headlessHace falta interacción, los datos no son accesibles de otra formaLento, pesadoSensible a la lógica de esperaBloquear imágenes, mantener baja la concurrencia

Casos típicos en los que sobra un navegador headless:

  • El contenido ya está en el código fuente de la página.
  • Los datos vienen de una petición JSON que se puede repetir con parámetros simples.
  • Los datos están en un bloque __NEXT_DATA__ o JSON-LD.
  • JavaScript solo se usa para navegar entre páginas, y el contenido llega del servidor en cada página.

Casos en los que un navegador headless hace falta de verdad:

  • El contenido solo se carga con scroll, clic o interacción con formularios, y la petición de API correspondiente no se puede repetir.
  • Las peticiones necesitan firmas o tokens de corta duración generados por el JavaScript de la página.
  • La salida visual de la página (una captura de pantalla, una verificación de diseño) es el propio trabajo.

Si usas un navegador headless, ajusta el número de páginas simultáneas a los recursos de la máquina; lo tratamos en Concurrencia y paralelismo.

Casos de uso

  • Seguimiento de precios en e-commerce: la página de categoría es dinámica, pero la lista de productos viene de una sola petición /api/products. La petición de API en lugar de un navegador headless; para ver precios en distintos países, un Proxies residenciales que sale por conexiones domésticas reales. La configuración está en nuestra página de solución de proxy para e-commerce.
  • Un sitio de anuncios hecho con Next.js: los datos están en __NEXT_DATA__. Basta con un cliente HTTP y analizar el JSON.
  • Una lista de reseñas con scroll infinito: se encuentra la petición de paginación que se envía al hacer scroll y se recorre en bucle con su parámetro cursor.
  • Un panel de cuenta que requiere interacción (tu propia cuenta): inicio de sesión, selección de filtros y descarga de informes con un navegador headless; la misma IP durante la sesión. La configuración general está en nuestra página de solución de extracción de datos.

Errores comunes

  • Pasar primero a un navegador headless. La petición JSON del panel Network suele dar los mismos datos mucho más barato.
  • Esperar un tiempo fijo. time.sleep(5) pierde tiempo con respuestas rápidas y sigue dando resultados vacíos con las lentas.
  • Esperar a networkidle. Puede no llegar nunca en páginas que siguen enviando peticiones.
  • Descargar todos los recursos. Las imágenes, las fuentes y los vídeos no hacen falta para los datos; bloquearlos ahorra tiempo y tráfico.
  • Confundir un resultado vacío con una página dinámica. Una página de verificación o un bloqueo también dan un resultado vacío; revisa el código de estado y el contenido.
  • Usar una API interna como si fuera pública. Las condiciones y los límites de velocidad del sitio también se aplican a estas peticiones.
  • Recurrir a herramientas de evasión de detección. Los plugins que intentan que el navegador «parezca humano» ocultan el origen del problema; revisar la velocidad, el alcance y el permiso es una vía más sólida. Explicamos por qué estas herramientas son problemáticas en Cómo usar undetected ChromeDriver para web scraping.

Guía de decisión

Resultado del diagnósticoRecomendación
El texto está en el código fuenteCliente HTTP + análisis de HTML
El código fuente tiene __NEXT_DATA__ o JSON-LDCliente HTTP + análisis de JSON
El panel Network tiene una petición JSON con los datosRepetir la petición directamente
La petición JSON necesita una firma de corta duraciónNavegador headless + expect_response
El contenido solo llega con interacciónNavegador headless + espera con locators
El navegador headless es lento y pesadoBloquear imágenes, bajar la concurrencia
No hay contenido en el HTML y el código es 403/429No es dinámica, es un bloqueo; diagnosticarlo

Preguntas frecuentes

¿Por qué requests no devuelve el contenido que veo en el navegador?

Porque requests solo obtiene el primer HTML que envía el servidor y no ejecuta JavaScript. Si el contenido de la página se carga después con JavaScript, ese HTML solo contiene el esqueleto. Busca en el panel Network la petición de la que vienen los datos, o JSON incrustado en el HTML.

¿Qué diferencia hay entre un navegador headless y uno normal?

El motor es el mismo; la diferencia es que en modo headless no se dibuja ninguna ventana en pantalla. La carga de la página, la ejecución de JavaScript y las peticiones de red funcionan como en un navegador normal. Algunos sitios pueden distinguir los navegadores headless por pequeñas diferencias.

¿Playwright o Selenium?

Ambos ejecutan páginas dinámicas. Playwright ofrece en una sola API funciones útiles para el scraping, como la espera automática, la captura de respuestas de red y el bloqueo de peticiones; Selenium es más antiguo, multilenguaje y tiene un ecosistema amplio. Seguir con el que ya usa tu proyecto suele ser lo más práctico.

Que una petición sea técnicamente pública no hace que su uso sea ilimitado. Ten en cuenta las condiciones de uso del sitio, las reglas de robots.txt y las leyes de datos personales; si hay una API oficial, prefiérela. Si tienes dudas, busca asesoramiento jurídico.

¿Cómo uso un navegador headless con un proxy?

En Playwright, el proxy se indica con el parámetro proxy al iniciar el navegador; la dirección del servidor, el usuario y la contraseña son campos separados. En Puppeteer y Selenium se pasa al navegador como argumento de inicio y la autenticación se gestiona aparte.

¿Cómo se obtienen datos de páginas con scroll infinito?

Primero, busca en el panel Network la petición de paginación que se envía al hacer scroll. Suele llevar un número de página o un valor cursor y se puede repetir en bucle. Si la petición no se puede repetir, haz scroll poco a poco en un navegador headless y espera en cada paso a que lleguen tarjetas nuevas.

En resumen

En una página estática el contenido está listo en el HTML y basta con una petición HTTP simple; en una página dinámica el contenido llega después a través de JavaScript y una petición HTTP devuelve un esqueleto. Antes de pasar a un navegador headless, mira el código fuente, busca en el panel Network la petición JSON con los datos y comprueba si hay datos incrustados en el HTML. Cuando un navegador sea realmente necesario, usa la espera con locators y expect_response en lugar de esperas fijas, bloquea las imágenes y mantén baja la concurrencia. Encontrarás tipos de proxy adecuados para tu trabajo de recogida de datos en nuestros servicios de proxy.