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:
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:
- El navegador recibe el esqueleto HTML y dibuja en pantalla un marcador como «Cargando...».
- Se descargan y ejecutan los archivos JavaScript. A menudo un framework como React o Vue.
- JavaScript envía una petición de API. Por ejemplo, con
fetcha/api/products?category=headphones&page=1. - El servidor devuelve los datos en JSON. Nombres de producto, precios, información de stock.
- JavaScript construye HTML a partir de ese JSON y lo inserta en la página. Las tarjetas de producto solo aparecen en este paso.
- 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:
- Mira el código fuente.
Ctrl + Uen la página (Cmd + Option + Uen 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. - 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.
- 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. - Compruébalo con código. Busca en el HTML obtenido con
requestsun texto que esperas. Si no está y el HTML es muy corto, has recibido un esqueleto. - 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:
- Abre las herramientas para desarrolladores (F12) y cambia a la pestaña Network.
- Marca Fetch/XHR en la barra de filtros.
- Recarga la página o realiza la acción que carga los datos (hacer scroll, elegir un filtro, pasar a la página siguiente).
- Haz clic en las peticiones cuyo nombre contenga palabras como
api,graphql,searchoproductsy mira el JSON en la pestaña Response. - 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:
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.
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:
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?
| Enfoque | Cuándo funciona | Velocidad y recursos | Fragilidad | A qué prestar atención |
|---|---|---|---|---|
| Cliente HTTP + HTML | La página es estática, el contenido está en el código fuente | Muy rápido, muy ligero | Sensible a cambios de diseño | Anclar los selectores a atributos estables |
| JSON incrustado en el HTML | Páginas híbridas como Next.js, Nuxt | Muy rápido, ligero | La estructura de datos puede cambiar | Gestión de errores que compruebe la ruta del JSON |
| Petición de API en segundo plano | El contenido llega en JSON | Muy rápido, el más ligero | Independiente del diseño, la API puede cambiar | Condiciones del sitio, tokens, límites de velocidad |
| Navegador headless | Hace falta interacción, los datos no son accesibles de otra forma | Lento, pesado | Sensible a la lógica de espera | Bloquear 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óstico | Recomendación |
|---|---|
| El texto está en el código fuente | Cliente HTTP + análisis de HTML |
El código fuente tiene __NEXT_DATA__ o JSON-LD | Cliente HTTP + análisis de JSON |
| El panel Network tiene una petición JSON con los datos | Repetir la petición directamente |
| La petición JSON necesita una firma de corta duración | Navegador headless + expect_response |
| El contenido solo llega con interacción | Navegador headless + espera con locators |
| El navegador headless es lento y pesado | Bloquear imágenes, bajar la concurrencia |
| No hay contenido en el HTML y el código es 403/429 | No 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.
¿Es legal usar una petición de API oculta?
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.




