Playwright es la biblioteca de automatización de navegadores de propósito general más sólida para web scraping en 2026: una sola API de Python controla Chromium, Firefox y WebKit, sus locators esperan automáticamente a que los elementos estén listos (lo que evita que los scripts fallen por problemas de tiempo), y page.route te permite bloquear recursos pesados o leer directamente las respuestas JSON de un sitio en lugar de analizar HTML. Esta guía te lleva desde pip install hasta un scraper funcional contra un sitio de pruebas, luego recorre las funciones avanzadas — bloqueo de recursos, intercepción de respuestas, contextos paralelos — y termina con la parte honesta que la mayoría de los tutoriales se saltan: qué tan detectable es Playwright en modo headless y cuánto cuesta realmente ejecutarlo a escala.
Por qué usar Playwright para scraping en 2026
Si la página que necesitas renderiza sus datos con JavaScript, requests más BeautifulSoup nunca los verá — esas herramientas leen el HTML crudo que envía el servidor, no lo que un navegador construye después. Esa combinación sigue siendo la opción correcta por defecto para páginas estáticas (empieza con nuestra guía de web scraping con Python y el tutorial de BeautifulSoup si ese es tu caso). Para todo lo demás necesitas un navegador headless, y Playwright se ha convertido en la opción por defecto por cuatro razones concretas:
- Locators con espera automática. Cada acción sobre un
Locator— hacer clic, leer texto, obtener un atributo — espera a que el elemento esté adjunto, visible y estable antes de ejecutarse, y reintenta hasta un timeout. Eltime.sleep(3)esparcido por los viejos scripts de Selenium simplemente desaparece. - Una API, tres motores.
p.chromium,p.firefoxyp.webkitcomparten la misma interfaz. Si un sitio se comporta distinto bajo el motor de Chrome, cambiar a Firefox es un cambio de una sola palabra. - Contextos de navegador. Un contexto es una sesión aislada, tipo incógnito, dentro de un mismo proceso de navegador — con sus propias cookies y su propio almacenamiento. Los contextos son baratos de crear y destruir, que es como Playwright logra paralelismo sin lanzar un navegador por cada tarea.
- Intercepción de red.
page.routese sitúa entre el navegador y la red, así que puedes abortar solicitudes de imágenes y fuentes, o capturar el JSON que el propio frontend de un sitio solicita — a menudo la fuente de datos más limpia de la página.
Frente a Selenium, la comparación honesta: Selenium tiene un ecosistema heredado más grande, más bindings de lenguaje, y Selenium Grid está muy probado para pruebas distribuidas. Pero la espera automática de Playwright, la intercepción de red integrada y el modelo de contextos hacen que el código de scraping sea más corto y menos inestable — Selenium necesita condiciones explícitas de WebDriverWait y una herramienta proxy como selenium-wire para cualquier cosa a nivel de red. Escribimos la comparación completa en Selenium web scraping; para proyectos de scraping nuevos, Playwright es el mejor punto de partida.
Frente a requests + BeautifulSoup: un navegador cuesta aproximadamente dos órdenes de magnitud más por página que una solicitud HTTP simple. Usa Playwright porque lo necesitas (renderizado de JavaScript, interacción del usuario, flujos de sesión), no porque sea la opción más llamativa.
Configuración y tu primer scrape
Dos comandos. El primero instala la biblioteca, el segundo descarga el binario del navegador — instala solo Chromium a menos que sepas que necesitas los demás:
pip install playwright
playwright install chromium
Aquí tienes un scraper completo y ejecutable contra books.toscrape.com, un sitio de pruebas diseñado para practicar:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://books.toscrape.com/")
books = []
for card in page.locator("article.product_pod").all():
books.append({
"title": card.locator("h3 a").get_attribute("title"),
"price": card.locator(".price_color").inner_text(),
"in_stock": "In stock" in card.locator(".availability").inner_text(),
})
print(f"scraped {len(books)} books")
print(books[0])
browser.close()
Ejecútalo y obtienes 20 diccionarios con título, precio y estado de stock. Fíjate en lo que no está en este script: sin esperas, sin sleeps, sin bucle de reintento. page.goto espera a que la página cargue, y cada acción de locator espera a su elemento. Eso es la espera automática haciendo su trabajo.
Una advertencia de la documentación oficial que conviene conocer desde el primer día: la API síncrona no es thread-safe. Una instancia de sync_playwright() por hilo, o usa la API asíncrona para concurrencia (más sobre eso abajo).
Locators bien usados
page.locator() acepta CSS (o XPath, detectado automáticamente), pero la documentación de Playwright te empuja hacia los locators orientados al usuario — y para scrapers ese consejo vale la pena, porque los roles y el texto visible sobreviven a rediseños que destrozan las cadenas de clases CSS:
# CSS — funciona bien para markup estable y semántico
page.locator("article.product_pod")
# Basado en rol — sobrevive a renombramientos de clases
page.get_by_role("link", name="next").click()
# Basado en texto — bueno para etiquetas y botones
page.get_by_text("Add to basket")
Los locators son estrictos por defecto: si un locator coincide con dos elementos y actúas sobre él, Playwright lanza un error en lugar de elegir el primero en silencio. Redúcelo con .filter(has_text="..."), .first, o .nth(i) cuando realmente quieras uno entre varios.
El patrón de referencia para scraping es: localizar el contenedor que se repite, llamar a .all(), y luego extraer campos relativos a cada elemento — exactamente lo que hizo el primer script. Extiéndelo con paginación y tienes un crawler real:
books = []
while True:
for card in page.locator("article.product_pod").all():
books.append({
"title": card.locator("h3 a").get_attribute("title"),
"price": card.locator(".price_color").inner_text(),
})
next_link = page.locator("li.next a")
if next_link.count() == 0:
break
next_link.click()
print(f"{len(books)} books across all pages")
count() es la comprobación idiomática de "¿existe esto?" — devuelve un resultado inmediato en lugar de esperar hasta un timeout como lo haría una acción sobre un elemento inexistente.
Funciones avanzadas para scrapers
Bloquear imágenes, fuentes y medios con page.route
Un scraper no necesita ver la página, así que no pagues por renderizarla. Registra un manejador de rutas antes de navegar y aborta los tipos de solicitud que no necesitas:
BLOCKED_TYPES = {"image", "font", "media"}
def block_heavy(route):
if route.request.resource_type in BLOCKED_TYPES:
route.abort()
else:
route.continue_()
page.route("**/*", block_heavy)
page.goto("https://books.toscrape.com/")
En páginas con muchas imágenes esto reduce los bytes transferidos a la mitad o más, y recorta segundos de cada navegación — los benchmarks de Scrapfly sitúan el ahorro de ancho de banda en 2–5x en sitios típicos. También puedes bloquear por patrón de URL (page.route("**/*.png", lambda r: r.abort())) para casos quirúrgicos. Dos advertencias: bloquear hojas de estilo puede romper selectores que dependen de la visibilidad, y bloquear scripts de analítica en algunos sitios con protección anti-bot te hace más sospechoso, no menos.
Interceptar respuestas JSON en lugar de hacer scraping del DOM
Los sitios modernos suelen cargar datos mediante llamadas XHR/fetch en segundo plano y renderizarlos del lado del cliente. Eso significa que los datos estructurados que quieres ya existen como JSON en la conexión — analizar el DOM después es hacer scraping de una copia con pérdida. Captura la respuesta directamente:
with page.expect_response("**/api/search*") as resp_info:
page.get_by_role("button", name="Search").click()
data = resp_info.value.json() # el payload estructurado propio del sitio
Para captura pasiva a lo largo de toda una sesión, adjunta un listener en su lugar:
captured = []
def on_response(response):
if response.request.resource_type in ("xhr", "fetch") and "/api/" in response.url:
captured.append(response.json())
page.on("response", on_response)
page.goto("https://example.com/catalog")
Cuando funciona, esto es estrictamente mejor que hacer scraping del DOM: campos tipados, sin mantenimiento de selectores, y a menudo campos extra que la página nunca muestra. Abre la pestaña Network de las DevTools en tu sitio objetivo antes de escribir ningún locator — puede que no los necesites.
Scraping en paralelo con contextos de navegador
No lances un navegador por cada URL. Lanza un navegador y abre muchos contextos — cada uno es una sesión aislada a una fracción del costo:
import asyncio
from playwright.async_api import async_playwright
async def scrape_one(browser, sem, url):
async with sem:
context = await browser.new_context()
page = await context.new_page()
await page.goto(url)
title = await page.locator("h1").inner_text()
await context.close()
return {"url": url, "title": title}
async def main(urls):
sem = asyncio.Semaphore(4) # 3–5 páginas concurrentes por máquina es realista
async with async_playwright() as p:
browser = await p.chromium.launch()
results = await asyncio.gather(*(scrape_one(browser, sem, u) for u in urls))
await browser.close()
return results
El semáforo importa: cada página abierta consume CPU y unos cientos de MB de memoria, y la concurrencia sin límites golpea al sitio objetivo hasta activar el rate limiting contra ti. Limita por dominio, no solo de forma global.
¿Síncrono o asíncrono?
Usa la API síncrona para scripts, tareas puntuales y cualquier cosa que ejecute un cron — es más fácil de leer y depurar. Recurre a la API asíncrona cuando necesites páginas concurrentes en un mismo proceso, o cuando tu scraper viva dentro de una aplicación asíncrona. Las dos APIs se corresponden método a método, así que actualizar más adelante es mecánico, no una reescritura.
Fallos comunes y cómo solucionarlos
| Fallo | Causa probable | Solución |
|---|---|---|
TimeoutError esperando un locator | El selector nunca coincide: CSS incorrecto, contenido dentro de un iframe, o el markup difiere de lo que viste en DevTools | Vuelve a ejecutar con headless=False y observa; prefiere get_by_role/get_by_text; usa page.frame_locator() para iframes |
goto con wait_until="networkidle" se cuelga o expira | El long-polling, los websockets o la analítica mantienen conexiones abiertas, así que la red nunca queda inactiva | No uses networkidle en sitios reales — espera el locator o la respuesta que realmente necesitas |
| Funciona con navegador visible, falla en headless | El modo headless reporta un viewport/fingerprint distinto; el contenido de carga diferida nunca se dispara fuera de pantalla | Configura un viewport explícito y un user_agent realista en el contexto; haz scroll con mouse.wheel para disparar la carga diferida |
| Violación de modo estricto: el locator resolvió a N elementos | Tu selector coincide con múltiples nodos y actuaste sobre él | Redúcelo con .filter(), .first, o .nth() — o arregla el selector, ya que la ambigüedad suele significar que está mal |
| Los datos están presentes en el navegador pero faltan en el scrape | Los datos llegan mediante un XHR retrasado después de la carga | Usa expect_response para la llamada a la API, o espera el locator específico que los renderiza |
¿Se puede detectar Playwright? Sí — aquí está la parte honesta
Fuera de la caja, Playwright en modo headless es detectable, y cualquier tutorial que se salte esto te está preparando para el fracaso. Las señales están bien documentadas: navigator.webdriver es true, la conexión CDP (Chrome DevTools Protocol) de la que depende Playwright deja artefactos en tiempo de ejecución que los scripts de detección buscan, y el fingerprint de navegador de Chromium headless — fuentes, cadenas del renderizador WebGL, listas de plugins ausentes, viewport por defecto — difiere de forma medible del navegador de un usuario real. Los proveedores de anti-bot prueban específicamente contra Playwright, porque todo el mundo lo usa.
Existen parches. playwright-stealth (un port de puppeteer-stealth) reescribe las propiedades de JavaScript más obvias, y las builds parcheadas van más allá. Pero esta es una carrera armamentista en la que estás en el bando equivocado: los proveedores de detección publican actualizaciones continuamente, y cada arreglo que aplicas es una firma que ellos ya recolectan. Espera CAPTCHAs, datos basura silenciosos y bloqueos de IP en objetivos protegidos — cubrimos la escalera de escalamiento en scraping de sitios que bloquean bots.
Después está el costo, que muerde incluso cuando la detección no lo hace. Una página de navegador necesita una porción real de CPU y cientos de MB de RAM; una máquina que envía miles de solicitudes HTTP simples por minuto apenas maneja unas pocas docenas de páginas de Playwright concurrentes. Añade proxies residenciales para sitios protegidos y la economía por página empeora aún más. Playwright es una herramienta excelente — para los sitios donde funciona, nada en Python es más agradable de usar. Pero "ejecutar más navegadores" es una respuesta cara para escalar.
Cuándo dejar de ejecutar navegadores
Para objetivos protegidos y de alto volumen, la jugada pragmática es dejar que alguien más se encargue de los navegadores, los proxies y el desbloqueo — y consumir JSON estructurado a través de una API HTTP normal. Eso es lo que hace la API de web scraping de Crawlora: endpoints documentados por plataforma, campos normalizados, sin parser ni fingerprint que mantener. Una búsqueda en Amazon es una sola solicitud simple:
import requests
resp = requests.get(
"https://api.crawlora.net/api/v1/amazon/search",
params={"k": "wireless earbuds"},
headers={"x-api-key": "YOUR_API_KEY"},
)
for item in resp.json()["data"][:3]:
print(item["title"], item["price"], item["rating"])
La facturación es pay-on-success — solo se te cobra por respuestas 2xx exitosas — y el nivel gratuito incluye 2,000 créditos al mes, sin tarjeta, suficiente para probar si los endpoints cubren tus objetivos antes de escribir código de navegador. Consulta precios para ver cómo se asignan los créditos a los endpoints, o prueba respuestas en vivo en el playground.
Una regla de decisión sensata para toda la pila:
- HTML estático, sin necesidad de renderizado con JavaScript — usa requests + BeautifulSoup, no un navegador.
- Páginas renderizadas con JavaScript, sin protección o con protección ligera — Playwright, con bloqueo de recursos e intercepción de respuestas.
- Los datos llegan mediante una llamada de API en segundo plano — intercepta el JSON; puede que no necesites locators en absoluto.
- Plataformas protegidas (retail, buscadores, redes sociales) a gran volumen — una API de scraping estructurada supera mantener parches de sigilo y pools de proxies.
- Carga de trabajo mixta — la mayoría de los proyectos reales usan Playwright para la cola larga y una API para los objetivos difíciles de alto volumen.
Evita la granja de navegadores para sitios protegidos
Endpoints documentados, JSON normalizado, proxies gestionados y desbloqueo. Facturación pay-on-success, y 2,000 créditos al mes, sin tarjeta.
Lecturas relacionadas
- Web scraping con Python: la guía completa — el pilar: elegir entre requests, BeautifulSoup y navegadores.
- Selenium web scraping — la comparación completa entre Playwright y Selenium para scrapers.
- Tutorial de BeautifulSoup — analizar HTML cuando no necesitas un navegador.
- Scraping de sitios que bloquean bots — cómo es la escalada de detección y tus opciones en cada escalón.
Preguntas frecuentes
Is Playwright better than Selenium for web scraping?
For new scraping projects, usually yes. Playwright's locators auto-wait and retry, network interception is built in, and browser contexts make parallel sessions cheap — all things Selenium needs explicit waits and add-ons like selenium-wire for. Selenium still has the larger legacy ecosystem and Selenium Grid for distributed testing.
Can Playwright be detected by websites?
Yes. Headless Playwright exposes navigator.webdriver, leaves CDP runtime artifacts that detection scripts probe for, and headless Chromium's fingerprint (fonts, WebGL, plugins, viewport) differs from a real browser. playwright-stealth patches the obvious leaks, but it is an ongoing arms race against anti-bot vendors that test against Playwright specifically.
Is Playwright free?
Yes. Playwright is an open-source library from Microsoft, free for commercial use, with first-class Python support. Your real costs are compute — each browser page needs CPU and hundreds of MB of RAM — plus proxies and unblocking on protected sites.
Should I use Playwright's sync or async API for scraping?
Use the sync API for scripts, one-off jobs, and cron tasks — it is easier to read and debug. Use the async API when you need concurrent pages in one process (with a semaphore capping 3-5 parallel pages per machine) or inside an async application. The APIs mirror each other, so migrating later is mechanical.
How do I make Playwright scraping faster?
Three levers: block images, fonts, and media with page.route (often halving page weight), intercept the site's own XHR/JSON responses instead of parsing the DOM, and parallelize with multiple browser contexts inside one browser process rather than launching a browser per URL.
Why does wait_until='networkidle' time out in Playwright?
Pages with long-polling, websockets, chat widgets, or streaming analytics never let the network go idle, so networkidle burns the full timeout on every navigation. Playwright's docs discourage it — wait for the specific locator or API response you need instead.
Can Playwright scrape JavaScript-heavy websites?
Yes — that is its main advantage over requests + BeautifulSoup. Playwright runs a real browser engine (Chromium, Firefox, or WebKit), executes the page's JavaScript, and its locators auto-wait for dynamically rendered content. You can also capture the underlying JSON API calls directly with page.expect_response.
