Saltar al contenido

Cómo automatizar capturas de sitios web con Playwright

Usa Playwright para automatizar capturas de sitios web con JavaScript. Captura una página, páginas completas, vistas móviles y lotes de URL con reintentos.

14 min de lecturaCaptura de sitios webPor Temposhot Team

Playwright es una biblioteca de automatización de navegadores de Microsoft. Controla Chromium, Firefox y WebKit desde Node.js, Python, Java o .NET. Esta publicación usa el paquete de Node.js para crear un script de capturas, empezando por una sola página y terminando con un lote de URL que reintenta los fallos.

Cuándo Playwright es la herramienta adecuada

Un script merece la configuración cuando las capturas forman parte de un proceso técnico más amplio:

  • las capturas se ejecutan en CI después de cada despliegue
  • la lista de URL procede de un sitemap, una base de datos o una API
  • los archivos deben guardarse en una estructura de carpetas fija o en un bucket de almacenamiento
  • el paso de capturas está junto a pruebas, extracción de datos o generación de PDF

Si la persona que necesita las imágenes no escribe código, o si la lista cambia cada semana, una herramienta sin código suele ser más rápida. Ese compromiso se trata al final.

El ejemplo práctico de esta publicación

Los ejemplos construyen hacia un único trabajo: capturar las 200 páginas que aparecen en el sitemap de un sitio después de cada despliegue, en tamaño de escritorio y de teléfono, e informar de las páginas que fallaron. Ese trabajo tiene todas las partes que hacen difíciles los scripts de capturas: una lista larga, páginas lentas, imágenes diferidas, banners de cookies y algún tiempo de espera agotado.

Las cifras de ese trabajo son estimaciones. Los tiempos reales dependen del sitio, de la red y de la máquina.

Cuatro cifras del trabajo de ejemplo: 200 URL, 400 capturas entre escritorio y teléfono, 4 workers en paralelo y una estimación de 10 a 20 minutos por ejecución.
La estimación supone de 3 a 6 segundos por página con 4 workers.

Lo que cuesta mantener el script

El script es gratuito, pero no está libre de mantenimiento. Un rediseño del sitio puede cambiar el banner de cookies, el elemento que se usa como señal de que la página está lista o el contenedor con desplazamiento, y cada cambio rompe una parte del lote.

Node.js u otro lenguaje

La API de capturas es casi idéntica en los cuatro lenguajes. page.screenshot(path=..., full_page=True) en Python hace lo mismo que fullPage: true en Node.js. Elige el lenguaje que ya usa el resto del proceso, para que el paso de capturas viva en el mismo repositorio y en el mismo trabajo de CI que el código que lo rodea.

Planifica el tiempo para eso. Un lote que se ejecuta después de cada despliegue compensa pronto el mantenimiento. Un lote que se ejecuta dos veces al año suele fallar en el primer intento, porque algo cambió entre ejecuciones.

Instala Playwright para un script de capturas

mkdir site-shots && cd site-shots
npm init -y
npm install -D playwright
npx playwright install chromium

El último comando descarga una compilación de Chromium que coincide con la versión instalada de Playwright. Los scripts de abajo usan la extensión .mjs, para que await de nivel superior funcione sin empaquetador ni configuración adicional.

Fija la versión y el navegador juntos

Cada versión de Playwright espera compilaciones concretas de los navegadores. Después de actualizar el paquete playwright, vuelve a ejecutar npx playwright install chromium, o el script fallará con un mensaje que indica que el ejecutable no existe.

Confirma package-lock.json para que cada equipo instale la misma versión. En un runner de CI con Linux, npx playwright install --with-deps chromium instala además las bibliotecas del sistema que necesita el navegador.

Lista de comandos: npx playwright install chromium descarga el navegador, install con --with-deps añade las bibliotecas de Linux, npx playwright screenshot hace una captura puntual y npx playwright codegen registra localizadores.

El comando screenshot es útil para comprobar una sola URL antes de que entre en el lote. También acepta --device y --wait-for-selector, así que una página lenta se puede probar desde la terminal.

Captura una captura de un sitio web

// one.mjs
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });

await page.goto('https://example.com');
await page.screenshot({ path: 'example.png' });

await browser.close();

Ejecútalo con node one.mjs. El navegador arranca en modo headless, lo que significa que renderiza la página sin abrir una ventana. La entrada del glosario navegador headless explica ese modelo.

Sin otras opciones, el archivo es una captura del viewport de exactamente 1920×1080 píxeles.

Opciones que hacen repetibles las capturas

Dos ejecuciones de la misma página deberían producir la misma imagen si nada ha cambiado. Algunas opciones de captura eliminan las fuentes habituales de ruido: animaciones captadas a mitad, un cursor de texto parpadeando y una densidad de píxeles que varía entre máquinas.

Panel de ajustes de las opciones de captura de Playwright: fullPage true, type jpeg, quality 80, animations desactivadas, caret oculto, scale css, lista mask de localizadores y timeout de 30000.
await page.screenshot({
  path: 'example.jpg',
  type: 'jpeg',
  quality: 80,
  animations: 'disabled',
  caret: 'hide',
  scale: 'css',
  mask: [page.locator('.ad-slot'), page.locator('.live-chat')],
});

mask pinta una caja sobre cada elemento que coincide. Es la forma más sencilla de evitar que los anuncios rotatorios, las marcas de hora en directo y los widgets de chat hagan que cada ejecución parezca distinta.

Depura una captura en blanco o incorrecta

Una captura en blanco, con una página de error o con una comprobación de bots necesita revisar qué vio el navegador. Lánzalo con headless: false y la opción slowMo para observar cada paso, o graba una traza y ábrela en el visor de trazas.

Cinco pasos numerados: volver a ejecutar solo esa URL con headless false, registrar el estado de la respuesta, grabar una traza con capturas e instantáneas, abrirla con npx playwright show-trace y después corregir la espera o el selector.

La traza muestra una tira de fotogramas de la página y las peticiones de red de cada acción. Una página que se renderizó por completo en la tira pero se capturó en blanco suele significar que la captura se ejecutó antes de que terminara un fundido de entrada, lo que corrige animations: 'disabled'.

Captura una página completa

Añade fullPage: true para capturar todo el documento desplazable.

await page.screenshot({ path: 'example-full.png', fullPage: true });

El ancho se mantiene en el ancho del viewport, y la altura crece para ajustarse a la página. Las páginas muy largas producen imágenes muy altas, que algunos visores de imágenes reducen mucho. Las páginas con cabeceras fijas pueden mostrar la cabecera una sola vez arriba, porque Playwright captura el documento en una sola pasada en lugar de unir vistas desplazadas.

Para capturar un solo elemento, llama a screenshot sobre un localizador: page.locator('main').screenshot({ path: 'main.png' }).

Captura una región fija

clip guarda un rectángulo de la página en píxeles CSS, indicado como x, y, width y height. Sirve para páginas en las que solo importan los primeros 1.200 píxeles, como una página de aterrizaje con la parte visible y la primera sección.

await page.screenshot({ path: 'hero.png', clip: { x: 0, y: 0, width: 1920, height: 1200 } });

Un recorte que se extiende más allá del final de una página corta se corta en el borde de la página. Comprueba la altura de la imagen cuando el recorte deba ser idéntico en todas las páginas.

Pon la URL de destino en un script por lotes

Guarda las URL en un archivo de texto para que nadie tenga que editar código para cambiar la lista.

// batch.mjs
import { readFile, mkdir } from 'node:fs/promises';
import { chromium } from 'playwright';

const urls = (await readFile('urls.txt', 'utf8'))
  .split('\n').map((l) => l.trim()).filter(Boolean);

await mkdir('shots', { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });

for (const [i, url] of urls.entries()) {
  const name = `shots/${String(i + 1).padStart(3, '0')}.png`;
  await page.goto(url, { waitUntil: 'load', timeout: 30000 });
  await page.screenshot({ path: name, fullPage: true });
  console.log('saved', url, name);
}

await browser.close();

Los nombres con ceros a la izquierda, como 001.png, mantienen los archivos en el orden de la lista en el Finder.

Construye la lista a partir de un sitemap

En el trabajo de ejemplo, la lista procede de sitemap.xml. Descárgalo una vez al inicio de la ejecución y extrae cada entrada loc, de modo que las páginas nuevas se capturen sin editar urls.txt.

const xml = await (await fetch('https://example.com/sitemap.xml')).text();
const urls = [...xml.matchAll(/<loc>(.*?)<\/loc>/g)].map((m) => m[1].trim());

Nombra los archivos por página, no solo por posición

Los nombres por índice, como 001.png, se rompen cuando el sitemap gana una página, porque cada archivo posterior se desplaza uno. Los nombres construidos con la ruta de la URL se mantienen estables entre ejecuciones: /pricing/teams se convierte en pricing-teams.png. Usa el índice como prefijo solo cuando el orden de la lista importe para la revisión.

Dos URL pueden asignarse al mismo nombre, por ejemplo una ruta con y sin barra final. Comprueba si hay nombres duplicados antes de la ejecución y añade el índice para resolverlos.

Los sitios grandes dividen su sitemap en un índice que apunta a más sitemaps. Sigue esos enlaces un nivel más abajo, y filtra las URL de archivos como los PDF, que no se renderizan como páginas.

Flujo de seis recuadros unidos por flechas: descargar sitemap.xml, crear la cola de URL, el worker abre la página y espera al elemento de listo, desplazarse para cargar las imágenes diferidas, guardar la captura, escribir failed.txt y un informe.

Añade concurrencia para lotes más grandes

Una página a la vez es lento para cientos de URL. Ejecuta algunas páginas en paralelo, cada una en su propio objeto de página, y limita el número para que la memoria siga bajo control.

const LIMIT = 4;
const queue = [...urls.entries()];

async function worker() {
  const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
  while (queue.length) {
    const [i, url] = queue.shift();
    await page.goto(url);
    await page.screenshot({ path: `shots/${i + 1}.png`, fullPage: true });
  }
  await page.close();
}

await Promise.all(Array.from({ length: LIMIT }, worker));

Cuatro workers es un punto de partida razonable en un portátil. Sube el límite solo después de comprobar el uso de memoria, y mantenlo bajo para los sitios que no son tuyos, para que la frecuencia de peticiones sea respetuosa.

Cómo cambia el tiempo de ejecución según el número de workers

El tiempo de ejecución baja casi en proporción al número de workers, hasta que la máquina o el sitio se convierten en el cuello de botella. Con una estimación de 4 segundos por página, 200 páginas tardan unos 13 minutos con 1 worker, unos 3,5 minutos con 4 y algo menos con 8.

A partir de ese punto, los workers adicionales sobre todo añaden presión de memoria y carga al servidor. Si las capturas empiezan a agotar el tiempo al subir el número, el sitio o la máquina están saturados, y el número debe bajar.

Mantén una frecuencia de peticiones respetuosa

Cada carga de página también carga scripts, imágenes y fuentes, así que 4 workers pueden significar decenas de peticiones por segundo a un mismo servidor. Para los sitios que no son tuyos, mantén el número de workers en 1 o 2 y añade una pausa de 1 a 2 segundos entre páginas. Ejecuta los lotes grandes fuera de las horas de mayor actividad del sitio, y detente cuando las respuestas empiecen a ser 429 Too Many Requests.

Páginas o contextos

El worker anterior comparte un único contexto de navegador, así que todas las páginas comparten cookies. Si una página establece una cookie de consentimiento, las páginas posteriores se saltan el banner, y las capturas difieren según el orden de los workers. Da a cada worker su propio contexto con browser.newContext() cuando cada captura deba empezar desde un estado limpio.

Captura vistas móviles o de tableta

Playwright incluye descriptores de dispositivos que definen a la vez el viewport, el user agent, el soporte táctil y la relación de píxeles.

import { chromium, devices } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({ ...devices['iPhone 13'] });
const page = await context.newPage();

Usa un contexto por dispositivo para que las cookies y el almacenamiento queden separados. Para una tableta, elige una entrada de iPad de la misma lista devices, o define un viewport simple como 768×1024.

Relación de píxeles y tamaño del archivo

Los descriptores de teléfono fijan deviceScaleFactor en 2 o 3. Un viewport de teléfono de 390 píxeles de ancho con un factor de 3 guarda una imagen de 1.170 píxeles de ancho, y una página larga puede alcanzar decenas de miles de píxeles de alto. Eso son nueve veces los píxeles de la misma página con un factor de 1.

Para revisar el diseño, scale: 'css' en las opciones de captura mantiene un píxel de imagen por cada píxel CSS. Conserva la escala del dispositivo solo cuando las imágenes deban verse nítidas en una pantalla de teléfono.

Confirma que se cargó el diseño móvil

Algunos sitios eligen el diseño según el user agent en el servidor, y otros según el ancho del viewport en CSS. Un descriptor define los dos, pero un simple cambio de viewport solo cambia el ancho. Si las capturas de teléfono muestran un diseño de escritorio apretado, pasa de un viewport simple a un descriptor completo de dispositivo.

WebKit para un renderizado parecido a Safari

Chromium es el valor predeterminado en la mayoría de los scripts, pero los diseños de iPhone se renderizan con WebKit en dispositivos reales. Instálalo con npx playwright install webkit y lánzalo con webkit.launch() cuando importe el renderizado específico de Safari.

Comparación con las columnas chromium y webkit, y filas para el comando de instalación, la proximidad a Safari en iPhone, el soporte de page.pdf y el uso típico en un lote de capturas.

Gestiona el contenido de carga diferida

Muchas páginas cargan las imágenes solo cuando entran en la vista al desplazarse. Una captura de página completa tomada justo después de la carga puede mostrar recuadros vacíos más abajo. Desplázate una vez por la página antes de la captura.

await page.evaluate(async () => {
  for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight) {
    window.scrollTo(0, y);
    await new Promise((r) => setTimeout(r, 200));
  }
  window.scrollTo(0, 0);
});

La entrada del glosario carga diferida explica por qué ocurre esto.

Espera a que las imágenes se hayan decodificado

Al desplazarse se inician las peticiones de imágenes, pero la captura todavía puede ejecutarse antes de que lleguen las imágenes. Después del desplazamiento, espera hasta que cada imagen de la página informe de que está completa.

await page.waitForFunction(() =>
  [...document.images].every((img) => img.complete && img.naturalWidth > 0)
);

Las URL de imagen rotas nunca se completan, así que esta espera puede agotar el tiempo en una página con un archivo que falta. Captura la excepción de tiempo agotado y haz la captura igualmente, y después marca la página en el informe.

Elige la estrategia de espera adecuada

La opción waitUntil decide cuándo devuelve el control goto.

ValorDevuelve el control cuando
loadSe dispara el evento load. Es el valor predeterminado.
domcontentloadedEl HTML está analizado, antes de que terminen las imágenes.
networkidleNo hay peticiones de red durante al menos 500 ms.

La documentación de Playwright desaconseja networkidle en las pruebas, porque las páginas con sondeos o analítica pueden no quedarse nunca en silencio. Un patrón más fiable consiste en esperar a un elemento que marque la página como lista, por ejemplo await page.locator('main img').first().waitFor().

Cronología de ejemplo de una página: goto empieza a los 0,0 segundos, el evento load hacia los 1,8 segundos, el elemento de listo visible hacia los 2,1 segundos, la pasada de desplazamiento termina hacia los 3,4 segundos, la captura se escribe hacia los 3,9 segundos y el límite de 30 segundos es el tope.
Valores de ejemplo, no una medición.
Filas de hacer y no hacer: esperar a un localizador que marque la página como lista, usar load como valor predeterminado de goto y fijar un tiempo límite de 30 segundos; no usar un waitForTimeout fijo de 5 segundos en todas las páginas, no usar networkidle en páginas con analítica y no capturar antes de que carguen las fuentes web.

Una espera fija de 5 segundos en 200 páginas añade unos 17 minutos de espera inactiva a un worker, y aun así falla en la página que necesita 6. Una espera por localizador devuelve el control en cuanto la página está lista. await page.evaluate(() => document.fonts.ready) cubre el caso de las fuentes.

Añade reintentos

Las redes fallan, y algunas páginas agotan el tiempo una vez y luego cargan bien. Envuelve la captura en una pequeña función de reintento.

async function capture(page, url, path, tries = 3) {
  for (let attempt = 1; attempt <= tries; attempt++) {
    try {
      await page.goto(url, { timeout: 30000 });
      await page.screenshot({ path, fullPage: true });
      return true;
    } catch (error) {
      console.warn(`attempt ${attempt} failed for ${url}: ${error.message}`);
    }
  }
  return false;
}

Recoge las URL que devuelven false y escríbelas en failed.txt para poder volver a ejecutarlas por separado.

No reintentes lo que no va a cambiar

Los reintentos ayudan con los tiempos agotados y las conexiones caídas. No ayudan con una página que devuelve 404 o 500, y tres intentos en 20 páginas que faltan desperdician minutos. page.goto devuelve un objeto de respuesta, así que comprueba response.status() y omite el reintento cuando el estado sea 400 o superior.

Registra el estado en el informe junto a la URL. Una lista de 404 procedentes de un sitemap es útil por sí sola, porque señala entradas de sitemap obsoletas.

Recupérate de una página bloqueada

Una página puede bloquearse en un sitio pesado, y todos los goto posteriores sobre ese objeto de página lanzan una excepción. Dentro de la función de reintento, cierra la página después de un error y abre una nueva para el siguiente intento. El worker sigue funcionando, y una URL defectuosa ya no hace fallar el resto de su cola.

Una estructura de carpetas práctica

site-shots/
  urls.txt
  batch.mjs
  shots/
    2026-03-24/
      desktop/001.png
      mobile/001.png
  failed.txt

Una carpeta con fecha por ejecución mantiene intactas las capturas anteriores. Una subcarpeta por dispositivo facilita la revisión lado a lado.

Qué debe subir una ejecución de CI

En CI, el runner se elimina al terminar el trabajo, así que las capturas deben subirse como artefactos de compilación. Sube la carpeta con fecha, la lista de fallos y un pequeño informe en JSON con la URL, el estado, el nombre del archivo y la duración de cada página.

Árbol de archivos de los artefactos de CI: una carpeta de ejecución con fecha que contiene carpetas de imágenes de escritorio y de teléfono, un archivo report.json con una entrada por URL y una lista failed.txt.
Lista de seis elementos: versión de Playwright fijada en el archivo de bloqueo, navegador instalado con sus dependencias, fuentes instaladas en el runner, locale y zona horaria fijados en el contexto, carpeta de ejecución subida como artefacto y trabajo marcado como fallido si failed.txt no está vacío.

Compara una ejecución con la anterior

Dos carpetas con fecha solo son útiles si alguien revisa las diferencias. Para unas pocas páginas, abre los archivos correspondientes en Compare Images, que tiene un deslizador y una vista de diferencias de píxeles. Para cientos de páginas, la aserción toHaveScreenshot de Playwright Test compara cada captura con una referencia guardada y falla si las diferencias superan un umbral.

Las fuentes son la diferencia más habitual entre una ejecución en un portátil y una en CI. Un runner de Linux sin las fuentes de respaldo del sitio renderiza el texto con anchos distintos, así que cambian los saltos de línea y las alturas de página.

Errores habituales

  1. Olvidar await. El script termina antes de que se escriba el archivo.
  2. Reutilizar una página después de un fallo. Abre una página nueva cuando goto lanza un error de destino cerrado.
  3. Depender de networkidle en todas partes. Algunas páginas nunca llegan a ese estado y cada fila agota el tiempo.
  4. Capturar banners de cookies. Haz clic en el botón de consentimiento con un localizador, u ocúltalo con page.addStyleTag.
  5. Ignorar las condiciones del sitio. Revisa las condiciones y las reglas de robots de los sitios que no son tuyos, y mantén la frecuencia baja.

Cierra un banner de cookies de forma fiable

Los banners cambian según el sitio, así que un solo selector rara vez funciona para una lista completa. Prueba una lista corta de botones de consentimiento conocidos con un tiempo límite de 2 segundos cada uno, y recurre a ocultar el contenedor del banner con CSS.

const consent = page.getByRole('button', { name: /accept|agree|reject all/i });
if (await consent.first().isVisible({ timeout: 2000 }).catch(() => false)) {
  await consent.first().click();
}

Hacer clic en un botón de consentimiento cambia las cookies para el resto del contexto. Con un contexto por worker, eso no supone problema; con un contexto compartido, es otra razón por la que las capturas difieren según el orden.

Alternativas a un script de capturas con Playwright

  • Puppeteer ofrece una API parecida para Chrome y Firefox. Consulta cómo automatizar capturas de sitios web con Puppeteer.
  • Playwright Test compara capturas con referencias. Consulta la guía de pruebas de capturas con Playwright.
  • Las API de capturas alojadas ejecutan el navegador por ti y devuelven imágenes por HTTP.
  • Website Capture de Temposhot ejecuta una lista de URL en local en un Mac sin código: Desktop 1920×1080, Tablet 768×1024 o Mobile 390×844, Viewport o Full page, hasta 10 navegadores a la vez, y Retry por fila.

Opciones del script y sus equivalentes sin código

La mayoría de las decisiones de esta publicación tienen un ajuste directo en Website Capture. La diferencia es quién toma la decisión: una persona desarrolladora en código, o cualquiera que pueda pegar una lista. Website Capture no tiene programador ni admite inicios de sesión, así que las ejecuciones en CI y las páginas tras un login se quedan con el script.

Comparación con las columnas script de Playwright y Website Capture, y filas para el viewport, la página completa, la condición de espera, el retardo fijo, el tipo y la calidad de imagen y el número de workers.

Preguntas frecuentes

¿Playwright necesita una ventana de navegador visible?

No. Los navegadores se lanzan en modo headless de forma predeterminada. Pasa headless: false a chromium.launch cuando quieras ver la página mientras depuras.

¿Playwright puede guardar JPEG en lugar de PNG?

Sí. Define type: 'jpeg' y un valor de quality de 0 a 100 en las opciones de captura, o usa un nombre de archivo .jpg.

¿Por qué se corta mi captura de página completa?

Algunas páginas se desplazan dentro de un elemento interno en lugar del documento. Captura ese elemento con una captura por localizador, o cambia su altura CSS antes de capturar.

¿Playwright puede capturar páginas tras un inicio de sesión?

Sí. Inicia sesión una vez, guarda la sesión con context.storageState({ path: 'auth.json' }) y crea los contextos posteriores con storageState: 'auth.json'. Mantén ese archivo fuera del control de versiones, porque contiene cookies de sesión.

¿Cómo evito que los anuncios y las marcas de hora cambien en cada captura?

Pasa sus localizadores en la opción mask. Playwright pinta una caja sólida sobre cada uno, así que las ejecuciones se pueden comparar sin diferencias falsas.

¿Por qué las capturas de CI se ven distintas de las de mi portátil?

Suelen diferir las fuentes, la relación de píxeles y el locale. Instala las mismas fuentes en el runner, define scale: 'css' y fija locale y timezoneId en el contexto para que las fechas y los formatos numéricos coincidan.

Referencias

¿Necesitas automatizar capturas de sitios web sin código?

Website Capture de Temposhot recibe una lista de URL pegada y captura cada página en tu Mac sin ningún script que mantener. Consulta automatización de capturas de sitios web o empieza con la guía de inicio rápido de Website Capture.

playwrightwebsite-captureautomationjavascript

Términos utilizados aquí: full page screenshot, playwright, puppeteer, screenshot api, screenshot automation, viewport screenshot

Convierte capturas repetibles en trabajo terminado

Temposhot toma capturas con un temporizador o en cada clic y exporta el resultado en PDF, imágenes, ZIP o MP4.

Publicaciones relacionadas