Come automatizzare gli screenshot di siti web con Puppeteer
Usa Puppeteer per automatizzare gli screenshot di siti web con JavaScript. Cattura una pagina, pagine intere, batch di URL e viewport mobile con uno script riutilizzabile.
13 min di letturaCattura siti webDi Temposhot Team
Puppeteer è una libreria Node.js mantenuta dal team di Chrome. Controlla Chrome o Firefox tramite un'API JavaScript ed è una scelta comune per gli screenshot di siti web automatizzati. Gli esempi seguenti partono da una cattura di cinque righe e arrivano a un runner riutilizzabile che legge gli URL da un file, esegue le pagine in parallelo e scrive un report.
L'ultima sezione mostra lo stesso batch come esecuzione di Temposhot Website Capture su un Mac, senza script da mantenere. I due approcci producono file comparabili, e la scelta dipende da dove viene eseguito il lavoro.
Quando Puppeteer è lo strumento giusto
Puppeteer è adatto quando gli screenshot fanno parte del codice che già esegui:
- un servizio Node.js che genera anteprime o immagini per i social
- un job cron o un passaggio CI che cattura le pagine dopo un deploy
- un job di scraping che richiede anche una registrazione visiva
- un team che usa già le funzionalità del Chrome DevTools Protocol
È meno adatto ai lavori una tantum svolti da persone che non mantengono progetti Node.js.
Installa Puppeteer per uno script di screenshot
mkdir site-shots && cd site-shots
npm init -y
npm install puppeteer
Il pacchetto puppeteer scarica durante l'installazione una build di Chrome compatibile. Il pacchetto più leggero puppeteer-core salta quel download e richiede invece il percorso di un browser già installato. Gli script seguenti usano file .mjs, così funziona il top-level await.
Scegli tra puppeteer e puppeteer-core
Usa puppeteer su una macchina di sviluppo dove il download è consentito. Usa puppeteer-core in un'immagine CI che ha già Chrome installato, o su una rete che blocca il download del browser. L'API è la stessa nei due pacchetti, quindi lo script non cambia. Serve un executablePath solo nella chiamata di avvio, quando il browser si trova in una posizione personalizzata.
Cattura lo screenshot di un sito
// one.mjs
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
await page.screenshot({ path: 'example.png' });
await browser.close();
page.setViewport imposta larghezza e altezza. Aggiungi deviceScaleFactor: 2 alla stessa chiamata per ottenere un'immagine con il doppio della densità di pixel, simile a un display Retina.
Cosa producono le impostazioni
Lo script sopra, con deviceScaleFactor: 2 aggiunto, ha una dimensione di output fissa. I numeri derivano dal viewport e dal fattore di scala.
L'output è il doppio del viewport in ciascuna direzione, perché il fattore di scala moltiplica i pixel mentre la dimensione CSS resta invariata. Un viewport 1440 per 900 con scala 1 produce un'immagine 1440 per 900, che è la dimensione giusta per un'anteprima web. Il fattore di scala serve a ottenere un'immagine nitida su uno schermo ad alta densità, e di conseguenza raddoppia la dimensione del file.
Cattura uno screenshot a pagina intera
await page.screenshot({ path: 'example-full.png', fullPage: true });
Con fullPage: true l'immagine copre l'intero documento scrollabile. Per un file più piccolo, salva in JPEG o WebP:
await page.screenshot({ path: 'example.jpg', type: 'jpeg', quality: 80, fullPage: true });
quality accetta valori da 0 a 100 e si applica solo a JPEG e WebP. Uno screenshot a pagina intera di una pagina lunga può essere alto decine di migliaia di pixel, quindi il JPEG spesso mantiene la dimensione del file gestibile.
Dimensione di una pagina lunga
La larghezza resta uguale a quella del viewport, mentre l'altezza segue il documento. Un articolo lungo rende l'immagine molto alta, e i numeri seguenti mostrano l'entità del cambiamento.
Un frame 3840 per 2160 ha circa 8,3 milioni di pixel, quindi l'immagine di esempio è circa il doppio di un frame 4K. Un'immagine alta è difficile da vedere in un unico pezzo, quindi dividi le pagine molto lunghe in sezioni con clip quando l'immagine verrà mostrata su schermo o in un documento.
Ritaglia una sezione
Usa l'opzione clip per salvare una sola regione. Le coordinate sono pixel CSS dall'alto della pagina, non dal viewport:
await page.screenshot({
path: 'example-pricing.png',
clip: { x: 0, y: 2400, width: 1440, height: 900 },
});
Usa fullPage o clip in una sola chiamata. Per dividere una pagina lunga in più file, leggi l'altezza con document.body.scrollHeight, poi crea un clip per ogni sezione. Una pagina di 12000 pixel divisa in sezioni da 900 pixel dà 14 file, e l'ultimo contiene solo 300 pixel.
Crea un runner riutilizzabile per URL
// run.mjs
import { readFile, mkdir, writeFile } from 'node:fs/promises';
import puppeteer from 'puppeteer';
const urls = (await readFile('urls.txt', 'utf8'))
.split('\n').map((l) => l.trim()).filter(Boolean);
const outDir = `shots/${new Date().toISOString().slice(0, 10)}`;
await mkdir(outDir, { recursive: true });
const browser = await puppeteer.launch();
async function shoot(url, index) {
const page = await browser.newPage();
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, { waitUntil: 'networkidle2', timeout: 45000 });
const path = `${outDir}/${String(index + 1).padStart(3, '0')}.jpg`;
await page.screenshot({ path, type: 'jpeg', quality: 80, fullPage: true });
return { url, path, ok: true };
} finally {
await page.close();
}
}
Una pagina nuova per ogni URL significa che un arresto anomalo su un sito non influisce sul successivo. La cartella di output datata mantiene intatte le esecuzioni precedenti.
Il runner ha cinque fasi. La figura seguente le elenca in ordine, con il file o il processo su cui ogni fase agisce.
Pulisci la lista degli URL
Righe vuote, commenti e URL duplicati fanno perdere tempo al browser, e un duplicato produce un secondo file per la stessa pagina. La versione seguente salta le righe che iniziano con il simbolo hash, rimuove i duplicati esatti ed elimina tutto ciò che non è un indirizzo HTTP o HTTPS. raw contiene il contenuto del file letto nel passaggio precedente.
const lines = raw.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
const urls = [...new Set(lines)].filter((l) => {
try {
const { protocol } = new URL(l);
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
});
Set confronta le stringhe in modo esatto, quindi https://example.com e https://example.com/ restano due URL. Registra anche le righe scartate. Un errore di battitura come htps://example.com compare così nel log all'inizio dell'esecuzione, invece di produrre un timeout 45 secondi dopo.
Numerazione per batch grandi
Lo script riempie l'indice con tre cifre, che si ordinano correttamente fino a 999 file. L'indice 1000 diventa 1000.jpg, e un ordinamento testuale lo colloca tra 100.jpg e 101.jpg. Usa invece la larghezza del batch:
const width = String(urls.length).length;
const path = `${outDir}/${String(index + 1).padStart(width, '0')}.jpg`;
Un batch di 1.250 URL ha larghezza 4, quindi il primo file è 0001.jpg. Calcola la larghezza una sola volta, prima che partano i worker.
Aggiungi concorrenza per batch più grandi
const LIMIT = 4;
const queue = [...urls.entries()];
const results = [];
async function worker() {
while (queue.length) {
const [i, url] = queue.shift();
results.push(await shootWithRetry(url, i));
}
}
await Promise.all(Array.from({ length: LIMIT }, worker));
await browser.close();
Ogni worker prende il successivo URL da una coda condivisa. Parti da 4 e aumenta il limite solo dopo aver verificato la memoria, perché ogni pagina aperta usa un proprio processo renderer.
Lo script apre ogni pagina da un solo browser. Le pagine condividono la cache del browser e, per impostazione predefinita, anche i cookie. Per un batch di siti pubblici non correlati, questo non crea problemi. Per un'esecuzione che non deve trasferire lo stato da un sito all'altro, crea un contesto per ogni URL con browser.createBrowserContext() e chiudilo dopo la cattura. Ogni contesto costa memoria, quindi questa scelta va valutata insieme al limite di concorrenza.
Scegliere il limite
Il limite stabilisce quante pagine vengono renderizzate contemporaneamente. Un limite basso mantiene stabile la memoria e allunga l'esecuzione, un limite alto fa l'opposto. La tabella seguente mostra il compromesso per un batch di 200 URL, dove ogni pagina richiede in media circa 6 secondi.
I tempi ideali presuppongono nessuna attesa di rete e nessun errore. I siti reali variano, e timeout e retry aggiungono tempo. Misura un batch di 20 URL con due limiti sulla macchina di destinazione e scegli il limite più alto che mantiene il Mac reattivo.
Cattura screenshot mobile
Puppeteer include i descrittori dei dispositivi tramite KnownDevices.
import puppeteer, { KnownDevices } from 'puppeteer';
const page = await browser.newPage();
await page.emulate(KnownDevices['iPhone 13']);
await page.goto('https://example.com');
page.emulate imposta in una sola chiamata viewport, rapporto di pixel, supporto touch e user agent. Per una semplice larghezza mobile senza modifiche allo user agent, chiama setViewport con isMobile: true e hasTouch: true.
Usa emulate quando un sito restituisce markup diverso in base allo user agent, e setViewport quando conta solo la larghezza del layout. Entrambe le chiamate si applicano alla pagina su cui vengono eseguite, quindi una seconda pagina aperta più tardi richiede una propria chiamata.
Un batch che mescola desktop e mobile richiede un viewport per ogni set di output, non uno per script. Tieni le immagini mobile in una cartella datata separata, per esempio shots/2026-04-10-mobile, così i numeri dei file dei due set non si sovrappongono. Temposhot Website Capture usa una dimensione Mobile di 390 per 844 per lo stesso scopo, impostata per riga nella stessa esecuzione.
Verifica il viewport emulato
Controlla la larghezza del layout nella pagina prima della cattura. Un descrittore sbagliato produce un layout desktop dentro un'immagine di dimensione mobile. Il valore seguente riporta la larghezza e il rapporto di pixel visti dalla pagina:
const seen = await page.evaluate(() => ({
width: window.innerWidth,
pixelRatio: window.devicePixelRatio,
}));
console.log(seen);
Per il descrittore iPhone 13 nelle versioni recenti di Puppeteer, la larghezza deve essere 390. Anche il rapporto di pixel deriva dal descrittore. Verifica i valori nella versione che installi, perché i dati dei descrittori cambiano tra le release.
Gestisci il contenuto caricato in modo lazy
Le immagini che si caricano allo scroll restano vuote in una cattura eseguita subito dopo la navigazione. Scorri prima la pagina:
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 questo schema.
Quanto dura lo scroll
Il ciclo attende 200 millisecondi per passo, e ogni passo avanza di un'altezza di viewport. L'esempio seguente è una pagina di 8000 pixel con un viewport di 900 pixel.
La pagina ha nove passi, quindi la passata aggiunge 1,8 secondi prima dello screenshot. Una pagina che carica le immagini lentamente richiede una pausa più lunga per ogni passo, e una pagina con scroll infinito non raggiunge mai una fine fissa. Per lo scroll infinito, fermati dopo un numero fisso di passi invece che all'altezza del documento.
Scorri dentro un contenitore
Alcuni layout fanno scorrere un elemento interno invece della finestra. Il ciclo sopra non vede alcun movimento, e la parte inferiore della pagina resta vuota. In quel caso scorri l'elemento tramite il suo scrollTop, usando come limiti il suo scrollHeight e il suo clientHeight. Dopo un'esecuzione, confronta il primo e l'ultimo frame. Due frame identici significano che la pagina non ha scorso, il che indica un contenitore.
Scegli la strategia di attesa corretta
| Opzione | Ritorna quando |
|---|---|
load | Si attiva l'evento load. È il valore predefinito. |
domcontentloaded | L'HTML è stato analizzato. |
networkidle0 | Nessuna connessione di rete per almeno 500 ms. |
networkidle2 | Non più di 2 connessioni per almeno 500 ms. |
networkidle0 può andare in timeout su pagine che eseguono polling o streaming. networkidle2 tollera quel traffico in background. Quando una pagina ha un segnale di pronto chiaro, page.waitForSelector su quell'elemento è la scelta più affidabile. Il timeout di navigazione predefinito è 30 secondi.
Scegli un solo segnale che la pagina imposta soltanto quando il contenuto è pronto. Un selettore presente in ogni caricamento, come l'header, non dice nulla sui dati sottostanti.
Misura prima di scegliere un'attesa
Misura il tempo di navigazione per ogni URL prima di decidere la strategia di attesa. Il valore seguente è il tempo fino all'evento load:
const started = Date.now();
await page.goto(url, { waitUntil: 'load', timeout: 45000 });
const loadMs = Date.now() - started;
Scrivi loadMs nel report accanto a ogni URL. Se la maggior parte delle pagine si carica in pochi secondi, l'attesa predefinita va bene. Se un gruppo di pagine si avvicina al timeout, passa quegli URL a domcontentloaded e attendi un selettore, poi conserva il report per verificare il cambiamento.
Aggiungi retry e report
async function shootWithRetry(url, i, tries = 3) {
for (let attempt = 1; attempt <= tries; attempt++) {
try {
return await shoot(url, i);
} catch (error) {
if (attempt === tries) return { url, ok: false, error: error.message };
}
}
}
// after Promise.all in run.mjs
await writeFile(`${outDir}/report.json`, JSON.stringify(results, null, 2));
Il report elenca ogni URL con il percorso del file o l'errore. Riporta le voci fallite in urls.txt per una seconda passata.
Costruisci la lista dei retry dal report
Il report trasforma la seconda passata in un filtro invece che in una copia manuale. Questo comando jq scrive gli URL falliti in un nuovo file:
jq -r '.[] | select(.ok == false) | .url' shots/2026-04-10/report.json > urls-retry.txt
Indica allo script urls-retry.txt per la seconda passata. Lo script dà alla cartella di output il nome della data, quindi una seconda esecuzione nello stesso giorno scrive nella stessa cartella. I file con gli stessi numeri sovrascrivono allora le immagini della prima esecuzione. Aggiungi un suffisso come -retry a outDir prima della seconda passata, così entrambe le esecuzioni restano nella cartella.
Tempo nel caso peggiore
Un sito lento può esaurire tutti i retry. Il timeout e il numero di tentativi fissano il caso peggiore per un URL, e il limite distribuisce quel costo tra i worker. La figura seguente usa i valori dello script.
Il caso peggiore è improbabile su un batch sano, ma fissa un limite massimo. Pianifica il job in modo che un'esecuzione con molti timeout termini comunque prima dell'inizio della successiva.
Ritenta solo gli errori che possono cambiare
Un timeout di navigazione o una connessione interrotta può riuscire al secondo tentativo. Una pagina che si carica completamente ma non contiene il selettore atteso di solito fallisce di nuovo allo stesso modo, e così una pagina che restituisce uno stato 404. Riprova il primo gruppo e registra il secondo una sola volta. La funzione shootWithRetry sopra riprova ogni errore, quindi un errore persistente costa tre timeout completi prima di comparire nel report.
Una struttura di cartelle pratica
site-shots/
run.mjs
urls.txt
shots/
2026-04-10/
001.jpg
002.jpg
report.json
Una cartella per ogni data di esecuzione, un report per cartella. È una struttura sufficiente per confrontare le esecuzioni in seguito senza un database.
Errori comuni
- Usare
networkidle0su ogni sito. Le pagine con widget di analytics o chat non si stabilizzano mai. - Lasciare le pagine aperte. La memoria cresce finché il processo rallenta o si blocca.
- Eseguire troppe pagine contemporaneamente. Un limite di 20 su un laptop causa spesso più timeout, non esecuzioni più veloci.
- Catturare overlay di consenso. Clicca il pulsante di rifiuto tramite selettore o nascondi il banner con
page.addStyleTag. - Ignorare i termini del sito. La cattura automatica invia richieste reali. Controlla i termini di ogni sito e mantieni basso il ritmo delle richieste.
Risoluzione dei problemi per sintomo
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| Area vuota in un'immagine a pagina intera | Il contenuto lazy non è mai entrato nella vista | Esegui la passata di scroll prima della cattura |
| Timeout su goto | La pagina non raggiunge mai l'evento di attesa | Attendi un selettore, oppure passa a domcontentloaded |
| Stessa immagine per ogni URL | La cattura è partita prima che finisse la navigazione | Attendi goto su ogni pagina e apri una nuova pagina per ogni URL |
| Immagine con dimensione doppia rispetto a quella attesa | deviceScaleFactor è 2 | Rimuovi il fattore o impostalo a 1 |
| La memoria cresce durante un'esecuzione lunga | Le pagine restano aperte dopo la cattura | Chiudi ogni pagina in un blocco finally e limita la concorrenza |
| Banner di consenso nell'immagine | Il banner non è stato gestito | Clicca il pulsante di rifiuto tramite selettore o nascondi il banner |
Alternative a uno script di screenshot con Puppeteer
- Playwright ha un'API simile e controlla anche WebKit. Vedi Come automatizzare gli screenshot di siti web con Playwright.
- API di screenshot ospitate eseguono il browser al posto tuo e restituiscono le immagini via HTTP.
- Estensioni del browser, come l'estensione Temposhot, salvano un solo PNG a pagina intera della scheda attiva.
- Temposhot Website Capture esegue su un Mac un elenco di URL incollato senza codice, con dimensioni Desktop 1920×1080, Tablet 768×1024 o Mobile 390×844 e un ritardo da 0 a 60 s per riga.
Quando lo script non vale la pena
Uno script costa più nel tempo di quanto suggerisca la sua prima versione. Ogni modifica del sito, aggiornamento di Node.js o di Chrome può richiedere una correzione. Un job mensile su una lista che cambia spesso costa meno con Website Capture, dove l'elenco degli URL e le impostazioni di riga stanno in un'unica finestra. Mantieni lo script quando le sue immagini alimentano un passaggio di build, quando una pipeline CI deve eseguirlo o quando i passaggi vanno oltre una visita e una cattura.
Lo stesso batch in Temposhot Website Capture
Temposhot Website Capture copre il batch di questo articolo senza script. Le impostazioni seguenti corrispondono al runner per la cattura a pagina intera 1440 per 900, con le opzioni di attesa e di blocco che Temposhot espone per riga.
La dimensione del dispositivo è diversa dallo script di proposito: in Temposhot Desktop è 1920 per 1080, mentre lo script usa 1440 per 900. Se gli output devono coincidere, imposta lo stesso viewport in entrambi. Al termine di un'esecuzione Temposhot, la barra dei risultati mostra ZIP, Images o PDF, che esporta i frame selezionati.
Ogni riga mostra il proprio stato durante l'esecuzione del batch, per esempio Capturing o Capture failed. Una riga fallita può essere riprovata da sola, senza riavviare il resto del batch.
FAQ
Puppeteer funziona con Firefox?
Sì. Le versioni recenti possono avviare Firefox oltre a Chrome. Chrome resta l'opzione più diffusa.
Puppeteer può salvare screenshot WebP?
Sì. Imposta type: 'webp' nelle opzioni dello screenshot. Il valore quality si applica a WebP come a JPEG.
Perché page.goto va in timeout su alcuni siti?
La pagina non ha raggiunto l'evento di attesa scelto entro 30 secondi. Passa a networkidle2 o load, aumenta timeout, oppure attendi un selettore.
Perché una parte della mia immagine a pagina intera è vuota?
Il contenuto si carica quando entra nella vista durante lo scroll. Esegui la passata di scroll della sezione sul lazy loading prima della cattura e attendi che compaia l'ultima riga.
Quanti URL può gestire una sola esecuzione?
Il limite dipende da memoria e tempo, non dal codice. Misura un batch di 20 URL con la concorrenza che prevedi di usare, osserva la memoria e scala di conseguenza. Temposhot Website Capture consente di impostare Concurrent browsers da 1 a 10, con 4 come valore predefinito.
L'output degli screenshot deve andare in Git?
No. Le immagini e il report sono output di build che crescono a ogni esecuzione. Conserva urls.txt e lo script nel controllo di versione, e salva la cartella di output dove la sua dimensione è sostenibile.
Riferimenti
Serve l'automazione di screenshot senza codice?
Temposhot Website Capture esegue lo stesso tipo di batch di URL in locale su un Mac, con impostazioni per riga e senza script da mantenere. Vedi automazione di screenshot di siti web oppure la guida alle opzioni di cattura.
puppeteerwebsite-captureautomationnodejs
Termini utilizzati in questa pagina: full page screenshot, headless browser, 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
- Come catturare in blocco screenshot di siti web su MacCattura in blocco screenshot di siti web su Mac con Temposhot. Incolla un elenco di URL, scegli i preset desktop, tablet e mobile, esegui la cattura in parallelo ed esporta per audit o report.
- Come automatizzare gli screenshot di siti web con PlaywrightUsa Playwright per automatizzare gli screenshot di siti web con JavaScript. Cattura una pagina, pagine intere, viste mobile e batch di URL con retry.
- Come automatizzare gli screenshot di siti web senza codiceScegli un modo no-code per automatizzare gli screenshot di siti web: cattura da una lista di URL, piattaforma di automazione con API per screenshot, strumenti di monitoraggio o helper del browser.
- Migliori alternative a Xnapper per Mac nel 2026Confronta le alternative a Xnapper per Mac: acquisizione ripetuta, acquisizione e registrazione a scorrimento, sfondi gratuiti, guide multipiattaforma e l'app integrata.