Automatizzare gli screenshot di siti web con Python per il monitoraggio
Cattura una pagina come PNG con una richiesta, scrivi i byte direttamente su file, e scegli il ramo giusto in base al content-type invece di indovinare quando una cattura fallisce. Testato ad agosto 2026.

Un controllo di visual regression deve vedere se una pagina è cambiata. Una status page ha bisogno di una thumbnail aggiornata di ogni sito che monitora. Un report settimanale deve mostrare come appariva davvero una pagina, non solo linkarla. Tutte e tre le cose hanno bisogno di un’immagine di un URL, non della descrizione di una, e /v1/screenshot prende un URL e restituisce un’immagine. La parte che frega tutti la prima volta è che la risposta non è affatto JSON, il che conta perché ogni altro endpoint FastAF lo è.
La risposta è byte, non JSON
/v1/screenshot restituisce dati immagine grezzi con un content-type di image/png. Non c’è nessun involucro, nessuna chiave results, niente su cui chiamare .json(). Scrivi il corpo della risposta direttamente su file:
import requests
API_KEY = "faf_live_..." # la tua chiave FastAF
resp = requests.get(
"https://api.fastaf.co/v1/screenshot",
params={"url": "https://example.com"},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
if resp.headers.get("content-type", "").startswith("image/"):
with open("screenshot.png", "wb") as f:
f.write(resp.content)
else:
print("capture failed:", resp.json()["error"])
Questo costa 5 crediti. Una cattura di default di https://example.com è tornata come PNG 1920x1080, 22.318 byte. Chiama resp.json() per abitudine e requests solleva un errore di decodifica, perché prova ad analizzare il corpo come testo JSON e qualche migliaio di byte di binario PNG non è JSON valido. resp.content è quello che vuoi, i byte grezzi; resp.text ti restituirebbe semplicemente quegli stessi byte decodificati come una stringa illeggibile, che non è quello che vuoi scrivere in un file .png.
Controllare il fallimento nel modo giusto
Una richiesta fallita non torna come un’immagine rotta, torna come JSON con un oggetto error. Questo è esattamente il motivo per cui il codice sopra controlla content-type prima di fare qualunque altra cosa, invece di assumere il successo e scoprire tre funzioni dopo che screenshot.png in realtà sono 40 byte di JSON con estensione .png. Il content-type è l’unico segnale che ti dice in modo affidabile che forma hai ricevuto, quindi scegli il ramo in base a quello per primo, ogni volta, invece di provare a scoprire un’immagine cattiva a posteriori, per esempio controllando il conteggio dei byte o aprendo il file con una libreria immagini e catturando qualunque eccezione lanci.
Per una dashboard di monitoraggio questo conta più di quanto sembri: il caso di fallimento è esattamente il momento in cui hai più bisogno di un segnale pulito, dato che di solito significa che la pagina target è down o sta dando errore, che è la cosa che stavi cercando di rilevare fin dall’inizio. Una pipeline che salva silenziosamente un blob di errore JSON come se fosse un PNG ti mostrerà una thumbnail rotta senza spiegazione invece del messaggio di errore vero e proprio che sta lì nella risposta.
Dimensioni e formato
Oltre a url, l’endpoint accetta: fullPage (true per catturare l’intera pagina scorrevole invece di un solo viewport), width e height per impostare il viewport prima della cattura, wait_until (load, domcontentloaded, o networkidle, per pagine che finiscono di renderizzare dopo l’evento di caricamento iniziale), format (png, jpg, o webp), e quality (da 0 a 100, per i formati lossy). wait_until è quello che conta di più per qualunque cosa costruita con un framework frontend moderno: una pagina può sparare il suo evento load prima che il JavaScript lato client abbia finito di disegnare il contenuto che vuoi davvero nell’immagine, quindi per una dashboard o una single-page app di solito conviene catturare con una condizione di attesa successiva a quella di default, al costo di una richiesta leggermente più lenta. Nessuno di questi parametri cambia il fatto che ricevi byte indietro, solo cosa ci finisce dentro:
resp = requests.get(
"https://api.fastaf.co/v1/screenshot",
params={
"url": "https://example.com",
"fullPage": "true",
"format": "jpg",
"quality": 80,
},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
Per la visual regression, tieni width, height, e format fissi tra le esecuzioni. Un diff tra due catture ha senso solo se le due immagini sono state prese alla stessa dimensione; cambiare il viewport tra un’esecuzione e l’altra ti mostrerà una differenza che non c’entra niente con la pagina.
Costruire un loop di monitoraggio
Un job di thumbnail per una status page o un report è un loop sugli URL, che salva ogni risposta sotto un nome che puoi ritrovare dopo e logga quelli che sono falliti invece di scartarli silenziosamente:
import os
def capture_all(urls, out_dir="captures"):
os.makedirs(out_dir, exist_ok=True)
for i, url in enumerate(urls):
resp = requests.get(
"https://api.fastaf.co/v1/screenshot",
params={"url": url, "width": 1280, "height": 800},
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=60,
)
if resp.headers.get("content-type", "").startswith("image/"):
with open(f"{out_dir}/{i}.png", "wb") as f:
f.write(resp.content)
else:
print(url, "failed:", resp.json()["error"])
Nelle nostre misurazioni la risposta mediana era 3,9 secondi. Va bene per una manciata di tile di dashboard su uno schedule; per qualche centinaio di pagine vorrai le richieste in esecuzione concorrente invece che una dopo l’altra, dato che un passaggio sequenziale su 500 URL a quella mediana da solo richiederebbe circa mezz’ora.
Quanto costa
| Volume | Crediti (30 giorni) | Pacchetto che lo copre | Costo |
|---|---|---|---|
| 100 pagine, controllate ogni giorno | 15.000 | Growth, $29 | ~$5,80 del pacchetto |
| 500 pagine, controllate ogni giorno | 75.000 | Growth, $29 | tutto il pacchetto |
| 500 pagine, controllate ogni 6 ore | 300.000 | Scale, $99 | tutto il pacchetto |
Le richieste fallite non vengono mai fatturate: i crediti vengono riservati quando una richiesta parte e restituiti se non torna con un risultato, quindi un target rotto non ti svuota silenziosamente il saldo. Il piano gratuito è 500 crediti senza carta, 100 screenshot per controllare tempi e dimensione dell’output sulle tue pagine prima di costruirci uno schedule attorno.