Captura de pantalla de una web con Node.js, Python y PHP
Necesitás una imagen de una página web desde tu código: para una miniatura, un reporte, un archivo de evidencia o un test visual. En tu máquina sale en cinco minutos; el problema aparece cuando eso tiene que correr en un servidor, todos los días, sin que nadie lo mire.
Hay dos caminos: levantar vos un navegador headless o pedirle la captura a una API. Vamos con los dos, con código que podés copiar y pegar.
Opción 1: hacelo vos con Puppeteer o Playwright
Puppeteer (Node.js) y Playwright (Node.js, Python y otros) controlan un Chrome sin ventana. Sacar una captura es corto:
// captura-puppeteer.mjs
// Instalación: npm install puppeteer
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto("https://example.com", { waitUntil: "networkidle2", timeout: 30000 });
await page.screenshot({ path: "captura.png", fullPage: true });
await browser.close();
Lo mismo en Python con Playwright:
# Instalación: pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 800})
page.goto("https://example.com", wait_until="networkidle", timeout=30000)
page.screenshot(path="captura.png", full_page=True)
browser.close()
Funciona. Los dolores aparecen cuando lo llevás a producción:
- Chrome en el servidor. Tenés que instalar el navegador y sus dependencias del sistema. En Docker o en un hosting compartido suele faltar alguna librería, las fuentes o los emojis, y la captura sale con cuadraditos donde había texto.
- Memoria. Cada pestaña de Chrome come bastante RAM. Con varias capturas en paralelo, un servidor chico se queda sin aire y el proceso muere.
- Timeouts y procesos colgados. Hay páginas que nunca terminan de cargar, que redirigen en loop o que se quedan esperando un script de terceros. Si no cerrás bien el navegador en cada error, te quedan procesos zombis que se acumulan.
- PHP. No hay una forma nativa: terminás llamando a un script de Node o a un binario de Chrome desde
exec(), cosa que muchos hostings no permiten. - Mantenimiento. Actualizar Chrome, ajustar la espera para páginas con animaciones, reintentar cuando falla. Es código que no es tu producto.
Si sacás pocas capturas y ya tenés un servidor con Node, esta opción está perfecta. Si no, seguí leyendo.
Opción 2: pedirle la captura a una API
Una API de capturas hace lo mismo, pero el Chrome corre en otro lado: vos mandás la URL y recibís la imagen. Los ejemplos usan screenshot.com.ar: GET /v1/capture devuelve la imagen directamente y te autenticás con el header X-API-Key. El plan gratis tiene 100 capturas por mes, sin tarjeta.
Los parámetros que vas a usar más seguido:
url: la página (obligatorio).widthyheight: el tamaño de la ventana, por defecto 1280 × 800.full_page=true: toda la página, no solo lo visible, hasta 10.000 px de alto.format:png(por defecto),jpegowebp.delay: milisegundos de espera antes de capturar (hasta 5.000), para páginas con animaciones.
Node.js (con fetch)
Necesitás Node 18 o superior, que ya trae fetch. Guardalo como captura.mjs y corré node captura.mjs:
// captura.mjs
import { writeFile } from "node:fs/promises";
const params = new URLSearchParams({
url: "https://example.com",
width: "1280",
full_page: "true",
format: "webp",
});
const res = await fetch(`https://screenshot.com.ar/v1/capture?${params}`, {
headers: { "X-API-Key": "TU_API_KEY" },
});
if (!res.ok) {
throw new Error(`Error ${res.status}: ${await res.text()}`);
}
await writeFile("captura.webp", Buffer.from(await res.arrayBuffer()));
// Si salió de cache no gastó nada y no trae este header
console.log("Listo. Capturas que te quedan:", res.headers.get("x-quota-remaining") ?? "(salió de cache)");
Python (con requests)
# Instalación: pip install requests
import requests
res = requests.get(
"https://screenshot.com.ar/v1/capture",
params={
"url": "https://example.com",
"width": 1280,
"full_page": "true",
"format": "webp",
},
headers={"X-API-Key": "TU_API_KEY"},
timeout=60,
)
if not res.ok:
raise SystemExit(f"Error {res.status_code}: {res.text}")
with open("captura.webp", "wb") as f:
f.write(res.content)
print("Listo. Salió de cache:", res.headers.get("X-Cache"))
PHP (con curl)
Funciona en cualquier hosting con la extensión curl, que viene activada casi siempre:
<?php
$params = http_build_query([
'url' => 'https://example.com',
'width' => 1280,
'full_page' => 'true',
'format' => 'webp',
]);
$ch = curl_init('https://screenshot.com.ar/v1/capture?' . $params);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: TU_API_KEY'],
CURLOPT_TIMEOUT => 60,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($body === false || $status !== 200) {
exit("Error $status: " . ($body === false ? curl_error($ch) : $body) . "\n");
}
file_put_contents('captura.webp', $body);
echo "Listo\n";
En los tres casos, no dejes la clave escrita en el código que subís a un repositorio: leela de una variable de entorno (process.env, os.environ o getenv()).
Detalles que te ahorran un rato
- Errores. Cuando algo falla, la respuesta es un JSON con
errorymessageen castellano. Los más comunes:render_failed(la página no cargó o bloquea navegadores),quota_exceeded(llegaste al límite del mes) ybusy(alta demanda, reintentá en unos segundos). - Páginas muy largas. Con
full_page=true, si la página pasa los 10.000 px de alto se captura la parte de arriba y la respuesta trae el headerX-Truncated: trueyX-Page-Heightcon el alto real. Si te importa, revisalo. - Cache. Si pedís la misma captura de nuevo dentro de las 24 horas, sale de cache (
X-Cache: HIT) y no cuenta en tu cuota. Si necesitás una captura fresca sí o sí, mandácache_ttl=0. - Peso. Para miniaturas,
format=webpojpegconqualityentre 60 y 80 pesan mucho menos que un PNG.
Bonus: el texto de la página en vez de la imagen
Si lo que querés es el contenido (para guardarlo, compararlo o pasárselo a un modelo de IA), está GET /v1/read: abre la página una vez y devuelve un JSON con el contenido principal en markdown, sin menús ni publicidad, y opcionalmente la captura en base64:
curl "https://screenshot.com.ar/v1/read?url=https://example.com&screenshot=false" \
-H "X-API-Key: TU_API_KEY"
¿Cuál conviene?
Si ya tenés la infraestructura y sacás pocas capturas, Puppeteer o Playwright alcanzan. Si no querés mantener un Chrome en el servidor, o estás en PHP, la API te lo resuelve con un pedido HTTP. Con 100 capturas gratis por mes podés probarla sin compromiso, y si te quedás corto los planes se pagan en pesos: mirá los precios. Todos los parámetros están en la documentación.
Probá screenshot.com.ar gratis
100 capturas por mes sin tarjeta, por API o por MCP. Planes en pesos con Mercado Pago.