Vai al contenuto

Come automatizzare gli screenshot di siti web con Playwright

Usa Playwright per automatizzare gli screenshot di siti web con JavaScript. Cattura una pagina, pagine intere, viste mobile e batch di URL con retry.

13 min di letturaCattura siti webDi Temposhot Team

Playwright è una libreria di automazione del browser di Microsoft. Pilota Chromium, Firefox e WebKit da Node.js, Python, Java o .NET. Questo articolo usa il pacchetto Node.js per costruire uno script di screenshot, partendo da una sola pagina e arrivando a un batch di URL che riprova le operazioni fallite.

Quando Playwright è lo strumento giusto

Uno script vale la configurazione quando gli screenshot fanno parte di un processo tecnico più ampio:

  • le catture vengono eseguite in CI dopo ogni deploy
  • l'elenco degli URL arriva da una sitemap, un database o un'API
  • i file devono finire in una struttura di cartelle fissa o in un bucket di storage
  • il passaggio dello screenshot si affianca a test, scraping o generazione di PDF

Se chi ha bisogno delle immagini non scrive codice, oppure se l'elenco cambia ogni settimana, uno strumento no-code è di solito più veloce. Questo compromesso è trattato alla fine.

L'esempio pratico di questo articolo

Gli esempi costruiscono verso un unico lavoro: catturare le 200 pagine elencate nella sitemap di un sito dopo ogni deploy, in formato desktop e telefono, e segnalare le pagine che non sono riuscite. Il lavoro ha tutte le parti che rendono difficili gli script di screenshot: un elenco lungo, pagine lente, immagini lazy, banner dei cookie e i timeout occasionali.

I numeri di questo lavoro sono stime. I tempi reali dipendono dal sito, dalla rete e dalla macchina.

Quattro statistiche per il lavoro di esempio: 200 URL, 400 screenshot tra desktop e telefono, 4 worker paralleli e una stima di 10-20 minuti per esecuzione.
La stima presuppone da 3 a 6 secondi per pagina con 4 worker.

Quanto costa gestire lo script

Lo script è gratuito, ma la manutenzione non è zero. Un redesign del sito può cambiare il banner dei cookie, l'elemento usato come segnale di pagina pronta o il contenitore con lo scroll, e ogni modifica interrompe una parte del batch.

Node.js o un altro linguaggio

L'API per gli screenshot è quasi identica nei quattro binding linguistici. page.screenshot(path=..., full_page=True) in Python fa la stessa cosa di fullPage: true in Node.js. Scegli il linguaggio che la pipeline usa già, così il passaggio di screenshot vive nello stesso repository e nello stesso job CI del codice che lo circonda.

Pianifica il tempo per questo. Un batch eseguito dopo ogni deploy ripaga rapidamente la manutenzione. Un batch eseguito due volte l'anno spesso fallisce al primo tentativo, perché nel frattempo qualcosa è cambiato.

Installa Playwright per uno script di screenshot

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

L'ultimo comando scarica una build di Chromium compatibile con la versione di Playwright installata. Gli script seguenti usano l'estensione .mjs, così il top-level await funziona senza bundler né configurazione aggiuntiva.

Fissa insieme la versione e il browser

Ogni release di Playwright richiede build specifiche dei browser. Dopo un aggiornamento del pacchetto playwright, esegui di nuovo npx playwright install chromium, altrimenti lo script fallisce con un messaggio che indica che l'eseguibile non esiste.

Committa package-lock.json in modo che ogni macchina installi la stessa versione. Su un runner CI Linux, npx playwright install --with-deps chromium installa anche le librerie di sistema necessarie al browser.

Elenco di comandi: npx playwright install chromium scarica il browser, install with-deps aggiunge le librerie Linux, npx playwright screenshot esegue una cattura singola, npx playwright codegen registra i locator.

Il comando screenshot è utile per controllare un singolo URL prima che entri nel batch. Accetta anche --device e --wait-for-selector, quindi una pagina lenta può essere testata dal terminale.

Cattura lo screenshot di un sito 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();

Eseguilo con node one.mjs. Il browser parte in modalità headless, cioè renderizza la pagina senza aprire una finestra. La voce headless browser spiega questo modello.

Senza altre opzioni, il file è uno screenshot del viewport di esattamente 1920×1080 pixel.

Opzioni che rendono le catture ripetibili

Due esecuzioni della stessa pagina dovrebbero produrre la stessa immagine se non è cambiato nulla. Alcune opzioni dello screenshot eliminano le fonti abituali di rumore: animazioni colte a metà, il cursore di testo lampeggiante e una densità di pixel che cambia tra macchine.

Pannello delle opzioni di screenshot di Playwright: fullPage true, type jpeg, quality 80, animazioni disattivate, caret nascosto, scale css, elenco mask di locator, timeout 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 disegna un riquadro sopra ogni elemento corrispondente. È il modo più semplice per evitare che annunci a rotazione, orari in tempo reale e widget di chat rendano ogni esecuzione diversa.

Risolvi una cattura vuota o errata

Uno screenshot vuoto, con una pagina di errore o con una verifica anti-bot va esaminato per capire cosa ha visto il browser. Avvia con headless: false e l'opzione slowMo per seguire ogni passaggio, oppure registra una trace e aprila nel trace viewer.

Cinque passaggi numerati: riesegui solo quell'URL con headless false, registra lo stato della risposta, registra una trace con screenshot e snapshot, aprila con npx playwright show-trace, poi correggi l'attesa o il selettore.

La trace mostra una sequenza di fotogrammi della pagina e le richieste di rete per ogni azione. Una pagina che nella sequenza risulta completamente renderizzata ma nella cattura è vuota di solito indica che lo screenshot è stato eseguito prima che finisse una dissolvenza in entrata. animations: 'disabled' risolve questo caso.

Cattura uno screenshot a pagina intera

Aggiungi fullPage: true per catturare l'intero documento scorrevole.

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

La larghezza resta quella del viewport e l'altezza cresce fino a contenere la pagina. Le pagine molto lunghe producono immagini molto alte, che alcuni visualizzatori riducono parecchio. Nelle pagine con header fissi, l'header compare una sola volta in alto, perché Playwright cattura il documento in un solo passaggio invece di unire viste scorse in sequenza.

Per catturare un singolo elemento, chiama screenshot su un locator: page.locator('main').screenshot({ path: 'main.png' }).

Cattura una regione fissa

clip salva un rettangolo della pagina in pixel CSS, indicato con x, y, width e height. È adatto alle pagine in cui conta solo la parte iniziale, come una landing page fino alla prima sezione.

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

Un clip che supera il fondo di una pagina corta viene tagliato al bordo della pagina. Controlla l'altezza dell'immagine quando il clip deve essere identico su tutte le pagine.

Inserisci gli URL in uno script batch

Tieni gli URL in un file di testo, così nessuno deve modificare il codice per cambiare l'elenco.

// 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();

I nomi con zeri iniziali come 001.png mantengono i file nell'ordine dell'elenco anche in Finder.

Crea l'elenco dalla sitemap

Per il lavoro di esempio, l'elenco arriva da sitemap.xml. Scaricala una volta all'inizio dell'esecuzione ed estrai ogni voce loc, così le nuove pagine vengono catturate senza modificare 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());

Nomina i file per pagina, non solo per posizione

I nomi con indice, come 001.png, si rompono quando la sitemap aggiunge una pagina, perché ogni file successivo scala di uno. I nomi costruiti dal percorso dell'URL restano stabili tra le esecuzioni: /pricing/teams diventa pricing-teams.png. Mantieni l'indice come prefisso solo quando l'ordine dell'elenco conta per la revisione.

Due URL possono produrre lo stesso nome, per esempio un percorso con e senza barra finale. Controlla i nomi duplicati prima dell'esecuzione e aggiungi l'indice per risolverli.

I siti grandi dividono la sitemap in un indice che punta ad altre sitemap. Segui quei link di un livello in giù e filtra gli URL di file come i PDF, che non vengono renderizzati come pagine.

Flusso di sei riquadri collegati da frecce: scarica sitemap.xml, costruisci la coda degli URL, il worker apre la pagina e attende l'elemento pronto, scorri per caricare le immagini lazy, salva lo screenshot, scrivi failed.txt e un report.

Aggiungi concorrenza per batch più grandi

Una pagina alla volta è lenta con centinaia di URL. Esegui alcune pagine in parallelo, ciascuna nel proprio oggetto page, e limita il numero per tenere sotto controllo la memoria.

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));

Quattro worker sono un buon punto di partenza su un laptop. Aumenta il limite solo dopo aver verificato l'uso della memoria, e tienilo basso per i siti che non possiedi, così la frequenza delle richieste resta corretta.

Come il numero di worker cambia il tempo di esecuzione

Il tempo di esecuzione scende quasi in proporzione al numero di worker, finché la macchina o il sito non diventano il collo di bottiglia. Con una stima di 4 secondi per pagina, 200 pagine richiedono circa 13 minuti con 1 worker, circa 3,5 minuti con 4 e un po' meno con 8.

Oltre questo punto, altri worker aumentano soprattutto la pressione sulla memoria e il carico sul server. Se gli screenshot iniziano ad andare in timeout quando aumenti il numero, il sito o la macchina sono saturi e il numero va riportato in basso.

Mantieni bassa la frequenza delle richieste

Ogni caricamento di pagina carica anche script, immagini e font, quindi 4 worker possono significare decine di richieste al secondo verso lo stesso server. Per i siti che non possiedi, tieni il numero di worker a 1 o 2 e aggiungi una pausa di 1-2 secondi tra una pagina e l'altra. Esegui i batch grandi fuori dalle ore di punta del sito e fermati quando le risposte diventano 429 Too Many Requests.

Pagine o context

Il worker sopra condivide un solo browser context, quindi tutte le pagine condividono i cookie. Se una pagina imposta un cookie di consenso, le pagine successive saltano il banner e le catture differiscono in base all'ordine dei worker. Assegna a ogni worker un proprio context con browser.newContext() quando ogni cattura deve partire da uno stato pulito.

Cattura screenshot mobile o tablet

Playwright include descrittori di dispositivo che impostano insieme viewport, user agent, supporto touch e rapporto pixel.

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 context per dispositivo, così cookie e storage restano separati. Per un tablet, scegli una voce iPad dalla stessa lista devices, oppure imposta un viewport semplice come 768×1024.

Rapporto pixel e dimensione del file

I descrittori per telefono impostano deviceScaleFactor a 2 o 3. Un viewport da telefono largo 390 pixel con fattore 3 salva un'immagine larga 1.170 pixel, e una pagina lunga può arrivare a decine di migliaia di pixel in altezza. Sono nove volte i pixel della stessa pagina con fattore 1.

Per la revisione del layout, scale: 'css' nelle opzioni di screenshot mantiene un pixel immagine per pixel CSS. Mantieni il fattore di scala del dispositivo solo quando le immagini devono risultare nitide su uno schermo di telefono.

Verifica che il layout mobile sia stato caricato

Alcuni siti scelgono il layout dallo user agent lato server, altri dalla larghezza del viewport in CSS. Un descrittore imposta entrambi, mentre una semplice modifica di viewport cambia solo la larghezza. Se le catture da telefono mostrano un layout desktop compresso, passa da un viewport semplice a un descrittore completo del dispositivo.

WebKit per un rendering simile a Safari

Chromium è il default nella maggior parte degli script, ma i layout di iPhone sono renderizzati da WebKit sui dispositivi reali. Installalo con npx playwright install webkit e avvialo con webkit.launch() quando conta il rendering specifico di Safari.

Confronto con colonne chromium e webkit, righe per il comando di installazione, la vicinanza a Safari su iPhone, il supporto a page.pdf e l'uso tipico in un batch di screenshot.

Gestisci i contenuti caricati in modo lazy

Molte pagine caricano le immagini solo quando entrano nella vista di scorrimento. Una cattura a pagina intera eseguita subito dopo il caricamento può mostrare riquadri vuoti più in basso. Scorri la pagina una volta prima dello screenshot.

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 voce lazy loading spiega perché succede.

Attendi che le immagini siano decodificate

Lo scroll avvia le richieste delle immagini, ma lo screenshot può ancora partire prima che le immagini arrivino. Dopo lo scroll, attendi che ogni immagine della pagina dichiari di essere completa.

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

Gli URL di immagini rotte non si completano mai, quindi questa attesa può scadere su una pagina con un file mancante. Intercetta il timeout, esegui comunque la cattura e segnala la pagina nel report.

Scegli la strategia di attesa giusta

L'opzione waitUntil decide quando goto restituisce il controllo.

ValoreRestituisce il controllo quando
loadL'evento load si attiva. È il default.
domcontentloadedL'HTML è analizzato, prima che finiscano le immagini.
networkidleNessuna richiesta di rete per almeno 500 ms.

La documentazione di Playwright sconsiglia networkidle per i test, perché le pagine con polling o analytics potrebbero non fermarsi mai. Un pattern più affidabile è attendere un elemento che segna la pagina come pronta, per esempio await page.locator('main img').first().waitFor().

Sequenza di esempio per una pagina: goto parte a 0,0 secondi, l'evento load arriva intorno a 1,8 secondi, l'elemento pronto è visibile intorno a 2,1 secondi, il passaggio di scroll termina intorno a 3,4 secondi, lo screenshot viene scritto intorno a 3,9 secondi, con un timeout di 30 secondi come limite massimo.
Valori di esempio, non una misura.
Righe do e dont: attendi un locator che segna la pagina come pronta, usa load come default di goto, imposta un timeout di 30 secondi; non usare un waitForTimeout fisso di 5 secondi su ogni pagina, non usare networkidle su pagine con analytics o chat, non catturare prima del caricamento dei font web.

Un'attesa fissa di 5 secondi su 200 pagine aggiunge circa 17 minuti di tempo inattivo a un solo worker, e fallisce comunque sulla pagina che ne richiede 6. Un'attesa su un locator restituisce il controllo appena la pagina è pronta. await page.evaluate(() => document.fonts.ready) copre il caso dei font.

Aggiungi i retry

Le reti falliscono e alcune pagine vanno in timeout una volta, per poi caricarsi correttamente. Avvolgi la cattura in un piccolo helper di retry.

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;
}

Raccogli gli URL per cui la funzione restituisce false e scrivili in failed.txt, così possono essere rieseguiti da soli.

Non ritentare ciò che non cambierà

I retry aiutano con i timeout e le connessioni interrotte. Non aiutano con una pagina che restituisce 404 o 500, e tre tentativi su 20 pagine mancanti fanno perdere minuti. page.goto restituisce un oggetto response, quindi controlla response.status() e salta il retry quando lo stato è 400 o superiore.

Registra lo stato nel report accanto all'URL. Un elenco di 404 proveniente dalla sitemap è utile di per sé, perché indica voci di sitemap obsolete.

Recupera da una pagina andata in crash

Una pagina può andare in crash su un sito pesante, e ogni goto successivo su quell'oggetto page lancia un errore. Dentro l'helper di retry, chiudi la pagina dopo un errore e aprine una nuova per il tentativo successivo. Il worker continua a girare e un URL problematico non fa più fallire il resto della sua coda.

Una struttura di cartelle pratica

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

Una cartella con data per ogni esecuzione conserva intatte le catture precedenti. Una sottocartella per dispositivo rende più semplice il confronto affiancato.

Cosa deve caricare un'esecuzione CI

In CI il runner viene eliminato al termine del job, quindi gli screenshot devono essere caricati come artifact di build. Carica la cartella con la data, l'elenco dei fallimenti e un piccolo report JSON con URL, stato, nome del file e durata per ogni pagina.

Albero di file degli artifact CI: una cartella di esecuzione con data che contiene le cartelle di immagini desktop e telefono, un file report.json con una voce per URL e un elenco failed.txt.
Checklist di sei voci: versione di Playwright fissata in package-lock.json, browser installato con le dipendenze, stessi font installati come sulla macchina di riferimento, locale e timezoneId impostati sul context, cartella dell'esecuzione caricata come artifact di build, job segnato come fallito quando failed.txt non è vuoto.

Confronta un'esecuzione con la precedente

Due cartelle con data servono solo se qualcuno ne esamina le differenze. Per poche pagine, apri i file corrispondenti in Compare Images, che include un cursore e una vista delle differenze pixel per pixel. Per centinaia di pagine, l'asserzione toHaveScreenshot di Playwright Test confronta ogni cattura con una baseline salvata e fallisce quando le differenze superano una soglia.

I font sono la differenza più comune tra un'esecuzione sul laptop e una in CI. Un runner Linux senza i font di fallback del sito renderizza il testo con larghezze diverse, quindi cambiano gli a capo e le altezze delle pagine.

Errori comuni

  1. Dimenticare await. Lo script termina prima che il file venga scritto.
  2. Riusare una pagina dopo un crash. Apri una nuova pagina quando goto lancia un errore di target chiuso.
  3. Affidarsi a networkidle ovunque. Alcune pagine non lo raggiungono mai e ogni riga va in timeout.
  4. Catturare i banner dei cookie. Clicca il pulsante di consenso con un locator, oppure nascondilo con page.addStyleTag.
  5. Ignorare i termini del sito. Controlla i termini d'uso e le regole robots dei siti che non possiedi, e mantieni bassa la frequenza delle richieste.

I banner cambiano da sito a sito, quindi un solo selettore raramente funziona per un intero elenco. Prova una breve lista di pulsanti di consenso noti con un timeout di 2 secondi per ciascuno, e ricorri a nascondere il contenitore 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();
}

Cliccare un pulsante di consenso cambia i cookie per il resto del context. Con un context per worker questo non è un problema; con un context condiviso è un altro motivo per cui le catture differiscono in base all'ordine.

Alternative a uno script di screenshot con Playwright

  • Puppeteer offre un'API simile per Chrome e Firefox. Vedi Come automatizzare gli screenshot di siti web con Puppeteer.
  • Playwright Test confronta gli screenshot con le baseline. Vedi la guida allo screenshot testing con Playwright.
  • API di screenshot ospitate eseguono il browser al posto tuo e restituiscono le immagini tramite HTTP.
  • Temposhot Website Capture esegue localmente su un Mac un elenco di URL senza codice: Desktop 1920×1080, Tablet 768×1024 o Mobile 390×844, Viewport o Full page, fino a 10 browser contemporaneamente e Retry per ogni riga.

Opzioni dello script e i loro equivalenti senza codice

La maggior parte delle scelte di questo articolo ha un'impostazione diretta in Website Capture. La differenza sta in chi fa la scelta: uno sviluppatore nel codice, oppure chiunque possa incollare un elenco. Website Capture non ha uno scheduler e non supporta l'accesso, quindi le esecuzioni in CI e le pagine dietro un login restano nello script.

Confronto con colonne Script Playwright e Website Capture, righe per viewport, fullPage, waitUntil, attesa extra, tipo e qualità dell'immagine e numero di pagine in parallelo.

FAQ

Playwright richiede una finestra del browser visibile?

No. I browser partono in modalità headless per impostazione predefinita. Passa headless: false a chromium.launch quando vuoi osservare la pagina durante il debug.

Playwright può salvare in JPEG invece che in PNG?

Sì. Imposta type: 'jpeg' e un valore quality da 0 a 100 nelle opzioni di screenshot, oppure usa un nome file .jpg.

Perché il mio screenshot a pagina intera è tagliato?

Alcune pagine scorrono dentro un elemento interno invece che nel documento. Cattura quell'elemento con uno screenshot su locator, oppure cambia la sua altezza CSS prima della cattura.

Playwright può catturare pagine dietro un login?

Sì. Accedi una volta, salva la sessione con context.storageState({ path: 'auth.json' }) e crea i context successivi con storageState: 'auth.json'. Tieni quel file fuori dal controllo di versione, perché contiene i cookie di sessione.

Come evito che annunci e timestamp cambino a ogni cattura?

Passa i loro locator nell'opzione mask. Playwright disegna un riquadro pieno su ciascuno, così le esecuzioni si possono confrontare senza differenze false.

Perché gli screenshot in CI sembrano diversi da quelli sul mio laptop?

Di solito cambiano font, rapporto pixel e locale. Installa gli stessi font sul runner, imposta scale: 'css' e fissa locale e timezoneId sul context, così date e formati numerici corrispondono.

Riferimenti

Ti serve l'automazione di screenshot senza codice?

Temposhot Website Capture accetta un elenco di URL incollato e cattura ogni pagina sul tuo Mac, senza script da mantenere. Vedi automazione di screenshot di siti web oppure inizia dalla guida rapida di Website Capture.

playwrightwebsite-captureautomationjavascript

Termini utilizzati in questa pagina: full page screenshot, playwright, puppeteer, screenshot api, screenshot automation, viewport screenshot

Trasforma gli screenshot ripetibili in lavoro completato

Temposhot scatta screenshot a intervalli regolari o a ogni clic ed esporta il risultato come PDF, immagini, ZIP o MP4.

Post correlati