Dramaturgo + Navegador de rastreo sin scrap: Intercepta la API JSON oculta
Senior Web Scraping Engineer
Una página que dice "citas" no envía citas. Solicite https://quotes.toscrape.com/scroll con un cliente HTTP simple y el cuerpo de la respuesta contiene un <div class="quotes"></div> vacío; el texto real, los autores y las etiquetas llegan después, a través de una llamada GET /api/quotes?page=N. El JavaScript de la página se ejecuta al cargar y en cada desplazamiento. Leer esa llamada directamente, en lugar de esperar a que un navegador lo convierta en HTML y luego extraiga el HTML, es la intercepción de solicitudes de red: conecta un navegador real a través del Protocolo de Herramientas de Desarrollo de Chrome, escucha el tráfico que genera y lee el JSON que el sitio ya produce para sí mismo.
Esta guía conecta Playwright al Navegador de Scraping Sin Esfuerzo a través de CDP, luego intercepta ese endpoint oculto de dos maneras: los propios eventos de respuesta de Playwright y el dominio CDP Network subyacente a ellos, antes de reproducir el endpoint directamente a través de HTTP simple una vez que su forma es conocida. Cada comando a continuación se ejecutó contra el objetivo en vivo.
Por Qué Leer el Tráfico en Lugar del DOM
quotes.toscrape.com/scroll es un objetivo público de práctica de scraping construido específicamente para demostrar desplazamiento infinito, y su marcado hace el caso por la intercepción por sí mismo. Obtenga la página y busque los datos:
bash
curl -s https://quotes.toscrape.com/scroll | grep -o 'class="quote"' | wc -l
Eso devuelve cero cada vez, porque las citas nunca residen en el HTML que el servidor envía. Un pequeño bloque de jQuery llama a $.get('/api/quotes', {page: page}) una vez al cargar y nuevamente cada vez que la posición de desplazamiento se acerca al final, luego agrega las filas devueltas en el contenedor vacío manualmente. Un navegador que ejecute el JavaScript y espere lo suficiente eventualmente mostrará las mismas 10 citas por página en su DOM; pero para cuando esté de regreso contando elementos .quote y extrayendo texto de nodos <span class="text"> y <small class="author">, ha reconstruido, a mano, datos que la página ya tenía como JSON limpio y tipado: un array quotes, cada uno con text, un objeto author y una lista tags, más una bandera has_next que le dice exactamente cuándo detenerse. Leer la respuesta directamente omite el paso de reconstrucción y le proporciona campos en lugar de nodos.
Requisitos Previos
Necesita Python 3.9 o más reciente; playwright 1.59.0 declara Requires-Python >=3.9 en PyPI; el propio paquete playwright, y una clave de API de Scraping Sin Esfuerzo del plan gratuito en app.scrapeless.com. La clave viaja como el parámetro de consulta token en el endpoint CDP del Navegador de Scraping, así que mantenlo en una variable de entorno en lugar de un literal en su script. No se requiere instalación de Chrome local: connect_over_cdp accede a un navegador que ya existe en la nube.
Conectar Playwright al Navegador de Scraping a Través de CDP
Instale el cliente y ajuste la clave:
bash
pip install playwright
bash
export SCRAPELESS_API_KEY="tu_clave_api_scrapeless"
El endpoint CDP del Navegador de Scraping es wss://browser.scrapeless.com/api/v2/browser, alcanzado con tres parámetros de consulta: token, sessionTTL y proxyCountry, el mismo constructor que utiliza cada script de Playwright sobre el Navegador de Scraping en esta serie:
python
import os
from urllib.parse import urlencode
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=180):
params = urlencode({
"token": API_KEY,
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
chromium.connect_over_cdp(scraping_browser_url()) devuelve un objeto Browser estándar de Playwright. Nada sobre los patrones de intercepción a continuación es específico del Navegador de Scraping; funcionan contra cualquier Chromium accesible a través de CDP; pero ejecutarlos en el Navegador de Scraping significa que el renderizado ocurre en infraestructura que ya lleva egress residencial y un Chromium anti-detección, por lo que los sitios que identifican agresivamente aún se hidratan normalmente mientras lee su tráfico.
Capturar la API Oculta con un Escuchador de Respuestas
page.expect_response() de Playwright vincula la espera a la acción que la desencadena: sin sueño arbitrario, sin consultar un recuento de selectores hasta que parezca estable. Vincúlelo alrededor de page.goto() para la primera llamada /api/quotes, y luego alrededor del desplazamiento que desencadena la segunda:
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=180):
params = urlencode({
"token": API_KEY,
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url())
page = browser.new_page()
with page.expect_response("**/api/quotes*") as first_page:
page.goto("https://quotes.toscrape.com/scroll", wait_until="domcontentloaded")
data = first_page.value.json()
es
print("Conteo de citas DOM antes de que llegue la primera respuesta:", page.locator(".quote").count())
print(f"página interceptada {data['page']} -> {len(data['quotes'])} citas, tiene_siguiente={data['has_next']}")
con page.expect_response("**/api/quotes*") como segunda_pagina:
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
data = segunda_pagina.value.json()
print("Conteo de citas DOM una vez que se asienta el desplazamiento:", page.locator(".quote").count())
print(f"página interceptada {data['page']} -> {len(data['quotes'])} citas, tiene_siguiente={data['has_next']}")
browser.close()
Ejecutarlo contra el sitio en vivo imprime:
text
Conteo de citas DOM antes de que llegue la primera respuesta: 10
página interceptada 1 -> 10 citas, tiene_siguiente=True
Conteo de citas DOM una vez que se asienta el desplazamiento: 20
página interceptada 2 -> 10 citas, tiene_siguiente=True
El patrón glob "**/api/quotes*" coincide con el punto final por forma de URL, que es el patrón documentado de Playwright para esperar una respuesta de red específica en lugar de un tiempo de espera fijo. Debido a que la espera está vinculada a la acción desencadenante, data['page'], data['quotes'], y data['has_next'] regresan como los mismos valores Python tipados que el propio script del sitio consume — sin .author.name reconstruido a partir de una etiqueta <small>, sin una lista de etiquetas reconstruida a partir del texto del ancla.
Ve Más Bajo: Lee Marcos Raw Con el Dominio de Red CDP
Los eventos de respuesta de Playwright se encuentran sobre el dominio de red del Protocolo de Herramientas de Desarrollo de Chrome, y puedes hablar con ese dominio directamente a través de una CDPSession. Esto importa cuando no estás controlando Playwright en absoluto — un cliente CDP puro en otro idioma, una herramienta que solo expone eventos de protocolo, o un caso donde quieres cabeceras de respuesta y tiempos que un enlace específico no expone — porque Network.responseReceived y Network.getResponseBody funcionan de la misma manera, independientemente de qué biblioteca cliente esté encima de ellas:
python
import os, json
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="US", session_ttl=180):
params = urlencode({
"token": API_KEY,
"sessionTTL": session_ttl,
"proxyCountry": proxy_country,
})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
request_ids, bodies = {}, []
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url())
page = browser.new_page()
cdp = page.context.new_cdp_session(page)
cdp.send("Network.enable")
def on_response_received(event):
if "/api/quotes" in event["response"]["url"]:
request_ids[event["requestId"]] = event["response"]["url"]
def on_loading_finished(event):
rid = event["requestId"]
if rid in request_ids:
raw = cdp.send("Network.getResponseBody", {"requestId": rid})
data = json.loads(raw["body"])
bodies.append((request_ids[rid], data["page"], len(data["quotes"]), data["quotes"][0]["author"]["name"]))
cdp.on("Network.responseReceived", on_response_received)
cdp.on("Network.loadingFinished", on_loading_finished)
page.goto("https://quotes.toscrape.com/scroll", wait_until="domcontentloaded")
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
for _ in range(20):
if len(bodies) >= 2:
break
page.wait_for_timeout(300)
for url, page_no, count, author in bodies:
print(f"CDP Network.getResponseBody en {url}: página={page_no}, citas={count}, primer_autor={author}")
browser.close()
text
CDP Network.getResponseBody en https://quotes.toscrape.com/api/quotes?page=1: página=1, citas=10, primer_autor=Albert Einstein
CDP Network.getResponseBody en https://quotes.toscrape.com/api/quotes?page=2: página=2, citas=10, primer_autor=Marilyn Monroe
Network.responseReceived se activa con cabeceras y un requestId tan pronto como la respuesta comienza; el cuerpo mismo no está disponible hasta que Network.loadingFinished confirma que la transferencia se completó, lo que es la razón por la que el controlador se divide en dos eventos en lugar de leer el cuerpo del primero. getResponseBody devuelve los bytes exactos que el navegador recibió, más una bandera base64Encoded para cargas útiles binarias — la capa sobre la que response.json() de Playwright se construye y oculta de ti.
Obtén un tiempo de ejecución de Scraping Browser gratuito registrándote en app.scrapeless.com y ejecutando ambos scripts anteriores contra tu propio objetivo.
Omite el Navegador Una Vez Que Conoces la Forma
Ambas interrupciones anteriores demostraron lo mismo: `/api/quotes?page=N` es un GET público y no autenticado que devuelve `quotes`, `page` y `has_next`. Una vez que conoces esa estructura, ya no se requiere un navegador para recorrerla; un cliente HTTP simple puede paginar toda la colección directamente:
```python
import json
import urllib.error
import urllib.request
def fetch_page(n):
url = f"https://quotes.toscrape.com/api/quotes?page={n}"
req = urllib.request.Request(url, headers={"User-Agent": "network-interception-demo/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
if resp.status != 200:
raise urllib.error.HTTPError(url, resp.status, "estado inesperado", resp.headers, None)
return json.loads(resp.read())
all_quotes, page = [], 1
while True:
data = fetch_page(page)
all_quotes.extend(data["quotes"])
if not data["has_next"]:
break
page += 1
print("páginas obtenidas:", page)
print("total de citas:", len(all_quotes))
print("primer autor de una cita:", all_quotes[0]["author"]["name"])
print("último autor de una cita:", all_quotes[-1]["author"]["name"])
text
páginas obtenidas: 10
total de citas: 100
primer autor de una cita: Albert Einstein
último autor de una cita: George R.R. Martin
Diez solicitudes HTTP simples extraen toda la colección de 100 citas a través del mismo contrato JSON que la sesión del navegador ya confirmó, sin que se ejecute ningún proceso de navegador en absoluto. Esta es la verdadera recompensa de la interceptación: el único trabajo del navegador aquí fue revelar el endpoint. Una vez que lo tienes, la forma más rápida de paginar a través de la colección suele ser detener la representación y llamar al endpoint directamente, siguiendo el estándar Fetch que implementan tanto los navegadores como los clientes HTTP simples.
Cuando aún necesitas el navegador
No cada endpoint oculto es tan cooperativo. Muchos requieren una cookie de sesión que el servidor establece durante una carga de página anterior, un token CSRF o de solicitud firmada integrado en el paquete JavaScript de la página, o un cuerpo de solicitud ensamblado a partir de un estado que solo existe del lado del cliente, y algunos se envían a través de un marco de WebSocket o un POST de GraphQL en lugar de un GET con forma REST como lo describe el estándar XMLHttpRequest. En esos casos, el paso de reproducción directa en la sección anterior no se aplica: no puedes reconstruir un encabezado de autenticación que nunca capturaste, pero el paso de interceptación sigue aplicándose. page.expect_response() y el dominio Network de CDP leen el tráfico de una página sin importar lo que lo autentique o qué forma tome la carga útil, porque observan lo que el navegador realmente envió y recibió en lugar de asumir un formato de solicitud particular de antemano. En esos sitios, mantén al navegador en el bucle para cada página: deja que cree la sesión, dirija la navegación y te entregue cada respuesta a medida que llega.
Para un ejemplo trabajado del mismo patrón de representar y luego leer la red aplicado a un sitio completo en lugar de un objetivo de práctica, consulta la guía de scraping de TikTok, que captura comentarios y publicaciones de XHR de la misma manera mientras desplaza un feed real.
La interceptación es un paso de descubrimiento tanto como uno de extracción. La primera vez que tocas un sitio no familiar, abre el panel de red de DevTools, filtra por Fetch/XHR y observa qué se activa mientras interactúas con la página; esa pasada manual es lo que te dice qué endpoint vincular a page.expect_response() antes de que escribas el script. Una vez que se conoce el endpoint, todo lo anterior —el oyente de respuesta, la sesión cruda de CDP y la reproducción directa— es ese mismo descubrimiento codificado como código que se ejecuta por sí mismo.
Regístrate en app.scrapeless.com para obtener un tiempo de ejecución gratuito de Scraping Browser, o consulta la página del producto Scraping Browser y precios para ejecuciones a gran escala.
FAQ
P: ¿Qué es la interceptación de solicitudes de red en el scraping web?
Es leer el tráfico XHR/fetch que el propio JavaScript de la página genera —normalmente, una llamada a la API JSON— en lugar de esperar a que ese tráfico se represente en HTML y luego analizar el marcado renderizado de vuelta.
P: ¿Es legal interceptar las propias llamadas de API de un sitio?
Las respuestas de lectura que tu navegador ya recibe al visitar una página pública tienen consideraciones diferentes a acceder a contenido detrás de autenticación o alcanzar datos no públicos. Limita cualquier flujo de trabajo a páginas públicas, respeta los términos de servicio del objetivo y las directivas de robots, y mantén el volumen de solicitudes controlado; trata la intercepción como una forma de leer tráfico más precisamente, no como una licencia para ignorar las reglas de acceso.
P: ¿Todavía necesitas un navegador una vez que conoces el endpoint oculto?
Solo si el endpoint requiere algo que el navegador proporciona: una cookie de sesión, un token firmado, un estado calculado por JavaScript. Un endpoint público y no autenticado como el que se describe en esta guía se puede reproducir con un cliente HTTP simple, como muestra el ejemplo de reproducción directa.
P: ¿Cuál es la diferencia entre page.expect_response() y escuchar el dominio Network del CDP sin procesar?
page.expect_response() es el envoltorio de nivel superior de Playwright: vincúlalo a un patrón de URL, activa la acción, recibe de vuelta un objeto Response parseado. El dominio Network del CDP es el protocolo subyacente — Network.responseReceived y Network.getResponseBody — útil cuando no estás usando un enlace de Playwright en absoluto, o necesitas detalles a nivel de protocolo que una biblioteca de cliente específica no expone.
P: ¿Esto funciona en endpoints que devuelven algo diferente a JSON?
Ambos patrones de intercepción leen cualquier byte que la respuesta lleve — response.text() o response.body() en Playwright, el campo body sin procesar de Network.getResponseBody en CDP — por lo que fragmentos HTML, XML, o cualquier otra carga útil se transmiten de la misma manera. JSON es simplemente el caso común para la API interna de una página.
P: ¿Puede page.expect_response() capturar solicitudes realizadas antes de que se cargue la página que estás observando?
No — tiene que estar escuchando antes de que se ejecute la acción desencadenante, por lo que envuelve la específica page.goto() o interacción que causa la solicitud, en lugar de estar adjunta después de que haya ocurrido.
P: ¿La intercepción necesita específicamente el Scrapeless Scraping Browser, o funciona con cualquier Chromium accesible a través de CDP?
La técnica en sí es un comportamiento genérico del CDP y funciona contra cualquier Chromium al que puedas acceder a través de connect_over_cdp, local o remoto. Ejecutarlo en Scrapeless Scraping Browser añade Chromium de anti-detección y salida residencial, lo que importa cuando el objetivo que estás interceptando tiene huellas suficientemente agresivas como para que un navegador local simple no pasara la renderización para producir el tráfico en primer lugar.
P: ¿Qué pasa si el sitio cambia su endpoint o la forma de la respuesta?
El código de intercepción sigue funcionando mientras el patrón de URL aún coincida; un campo renombrado o un payload reestructurado rompe el código que lee data['quotes'], de la misma manera que un selector de CSS se rompe cuando cambia un nombre de clase. Ninguno de los enfoques es inmune a un rediseño: leer la API simplemente significa que estás rastreando un contrato JSON en lugar de una estructura de marcado, que tiende a cambiar con menos frecuencia.
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.



