Web Scraping con un browser senza testa: un tutorial per burattinai
Pubblicato: 2022-03-11In questo articolo vedremo com'è facile eseguire il web scraping (web automation) con il metodo in qualche modo non tradizionale di utilizzare un browser headless .
Che cos'è un browser senza testa e perché è necessario?
Gli ultimi anni hanno visto il Web evolversi da siti Web semplicistici costruiti con HTML e CSS semplici. Ora ci sono molte più app Web interattive con bellissime UI, che sono spesso costruite con framework come Angular o React. In altre parole, al giorno d'oggi JavaScript governa il web, incluso quasi tutto ciò con cui interagisci sui siti web.
Per i nostri scopi, JavaScript è un linguaggio lato client. Il server restituisce file JavaScript o script inseriti in una risposta HTML e il browser lo elabora. Ora, questo è un problema se stiamo facendo una sorta di web scraping o automazione web perché il più delle volte, il contenuto che vorremmo vedere o scrappare è effettivamente visualizzato dal codice JavaScript e non è accessibile dalla risposta HTML grezza che il server fornisce.
Come accennato in precedenza, i browser sanno come elaborare JavaScript e visualizzare bellissime pagine Web. E se potessimo sfruttare questa funzionalità per le nostre esigenze di scraping e avere un modo per controllare i browser in modo programmatico? È proprio qui che interviene l'automazione del browser senza testa!
Senza testa? Mi scusi? Sì, questo significa solo che non esiste un'interfaccia utente grafica (GUI). Invece di interagire con gli elementi visivi come faresti normalmente, ad esempio con un mouse o un dispositivo touch, automatizzi i casi d'uso con un'interfaccia a riga di comando (CLI).
Chrome senza testa e burattinaio
Esistono molti strumenti di web scraping che possono essere utilizzati per la navigazione senza testa, come Zombie.js o Firefox senza testa utilizzando Selenium. Ma oggi esploreremo Chrome senza testa tramite Burattinaio, poiché è un giocatore relativamente nuovo, rilasciato all'inizio del 2018. Nota del redattore: vale la pena menzionare il browser remoto di Intoli, un altro nuovo giocatore, ma questo dovrà essere un argomento per un altro articolo.
Cos'è esattamente Burattinaio? È una libreria Node.js che fornisce un'API di alto livello per controllare Chrome o Chromium senza testa o per interagire con il protocollo DevTools. È gestito dal team di Chrome DevTools e da una fantastica community open source.
Basta parlare: entriamo nel codice ed esploriamo il mondo di come automatizzare lo scraping web utilizzando la navigazione senza testa di Puppeteer!
Preparare l'ambiente
Prima di tutto, devi avere Node.js 8+ installato sul tuo computer. Puoi installarlo qui, o se sei un amante della CLI come me e ti piace lavorare su Ubuntu, segui questi comandi:
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
Avrai anche bisogno di alcuni pacchetti che potrebbero essere o meno disponibili sul tuo sistema. Per sicurezza, prova a installarli:
sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
Configura Chrome senza testa e Burattinaio
Consiglierei di installare Puppeteer con npm
, poiché includerà anche la versione stabile di Chromium aggiornata che è garantita per funzionare con la libreria.
Esegui questo comando nella directory principale del tuo progetto:
npm i puppeteer --save
Nota: l'operazione potrebbe richiedere del tempo poiché Burattinaio dovrà scaricare e installare Chromium in background.
Ok, ora che siamo tutti pronti e configurati, inizia il divertimento!
Utilizzo dell'API Puppeteer per il web scraping automatizzato
Iniziamo il nostro tutorial Burattinaio con un esempio di base. Scriveremo uno script che farà sì che il nostro browser senza testa acquisisca uno screenshot di un sito Web di nostra scelta.
Crea un nuovo file nella directory del tuo progetto chiamato screenshot.js
e aprilo nel tuo editor di codice preferito.
Per prima cosa, importiamo la libreria Puppeteer nel tuo script:
const puppeteer = require('puppeteer');
Successivamente, prendiamo l'URL dagli argomenti della riga di comando:
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }
Ora, dobbiamo tenere a mente che Puppeteer è una libreria basata su promesse: esegue chiamate asincrone all'istanza di Chrome senza testa sotto il cofano. Manteniamo pulito il codice usando async/await. Per questo, dobbiamo prima definire una funzione async
e inserire tutto il codice Burattinaio:
async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
Complessivamente, il codice finale è simile a questo:
const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
Puoi eseguirlo eseguendo il seguente comando nella directory principale del tuo progetto:
node screenshot.js https://github.com
Aspetta un secondo e boom! Il nostro browser headless ha appena creato un file chiamato screenshot.png
e puoi vedere la home page di GitHub renderizzata al suo interno. Ottimo, abbiamo un web scraper Chrome funzionante!
Fermiamoci per un minuto ed esploriamo cosa succede nella nostra funzione run()
sopra.
Innanzitutto, avviamo una nuova istanza del browser senza testa, quindi apriamo una nuova pagina (scheda) e passiamo all'URL fornito nell'argomento della riga di comando. Infine, utilizziamo il metodo integrato di Burattinaio per acquisire uno screenshot e dobbiamo solo fornire il percorso in cui dovrebbe essere salvato. Dobbiamo anche assicurarci di chiudere il browser senza testa dopo aver terminato con la nostra automazione.
Ora che abbiamo coperto le basi, passiamo a qualcosa di un po' più complesso.
Un secondo esempio di raschiamento del burattinaio
Per la parte successiva del nostro tutorial sui burattinai, diciamo di voler racimolare gli articoli più recenti di Hacker News.
Crea un nuovo file chiamato ycombinator-scraper.js
e incolla il seguente frammento di codice:
const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);
Va bene, c'è un po' di più qui rispetto all'esempio precedente.
La prima cosa che potresti notare è che la funzione run()
ora restituisce una promessa, quindi il prefisso async
si è spostato nella definizione della funzione di promessa.
Abbiamo anche racchiuso tutto il nostro codice in un blocco try-catch in modo da poter gestire eventuali errori che causano il rifiuto della nostra promessa.
E infine, stiamo usando il metodo integrato di Puppeteer chiamato evaluate()
. Questo metodo ci consente di eseguire codice JavaScript personalizzato come se lo stessimo eseguendo nella console DevTools. Tutto ciò che viene restituito da quella funzione viene risolto dalla promessa. Questo metodo è molto utile quando si tratta di raschiare informazioni o eseguire azioni personalizzate.
Il codice passato al metodo evaluate()
è JavaScript piuttosto semplice che costruisce una matrice di oggetti, ciascuno con campi url
e di text
che rappresentano gli URL della storia che vediamo su https://news.ycombinator.com/.
L'output dello script è simile a questo (ma con 30 voci, originariamente):
[ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]
Abbastanza carino, direi!
Va bene, andiamo avanti. Abbiamo solo 30 articoli restituiti, mentre ce ne sono molti altri disponibili, sono solo su altre pagine. Dobbiamo fare clic sul pulsante "Altro" per caricare la pagina successiva dei risultati.

Modifichiamo un po' il nostro script per aggiungere un supporto all'impaginazione:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
Rivediamo cosa abbiamo fatto qui:
- Abbiamo aggiunto un singolo argomento chiamato
pagesToScrape
alla nostra funzione principalerun()
. Lo useremo per limitare il numero di pagine che il nostro script rassegnerà. - C'è un'altra nuova variabile chiamata
currentPage
che rappresenta il numero della pagina di risultati che stiamo guardando attualmente. Inizialmente è impostato su1
. Abbiamo anche racchiuso la nostra funzione dievaluate()
in un ciclowhile
, in modo che continui a funzionare finchécurrentPage
è minore o uguale apagesToScrape
. - Abbiamo aggiunto il blocco per passare a una nuova pagina e attendere il caricamento della pagina prima di riavviare il ciclo
while
.
Noterai che abbiamo utilizzato il metodo page.click()
per fare in modo che il browser senza testa faccia clic sul pulsante "Altro". Abbiamo anche usato il metodo waitForSelector()
per assicurarci che la nostra logica sia sospesa fino al caricamento del contenuto della pagina.
Entrambi sono metodi API Puppeteer di alto livello pronti per l'uso pronti per l'uso.
Uno dei problemi che probabilmente incontrerai durante lo scraping con Puppeteer è l'attesa del caricamento di una pagina. Hacker News ha una struttura relativamente semplice ed è stato abbastanza facile attendere il completamento del caricamento della pagina. Per casi d'uso più complessi, Puppeteer offre un'ampia gamma di funzionalità integrate, che puoi esplorare nella documentazione dell'API su GitHub.
Tutto ciò è piuttosto interessante, ma il nostro tutorial sui burattinai non ha ancora trattato l'ottimizzazione. Vediamo come possiamo far correre Puppeteer più velocemente.
Ottimizzazione del nostro copione burattinaio
L'idea generale è di non lasciare che il browser senza testa faccia alcun lavoro extra. Ciò potrebbe includere il caricamento di immagini, l'applicazione di regole CSS, l'attivazione di richieste XHR, ecc.
Come con altri strumenti, l'ottimizzazione di Burattinaio dipende dal caso d'uso esatto, quindi tieni presente che alcune di queste idee potrebbero non essere adatte al tuo progetto. Ad esempio, se avessimo evitato di caricare le immagini nel nostro primo esempio, il nostro screenshot potrebbe non avere l'aspetto che volevamo.
Ad ogni modo, queste ottimizzazioni possono essere realizzate memorizzando nella cache le risorse alla prima richiesta o annullando le richieste HTTP a titolo definitivo quando vengono avviate dal sito Web.
Vediamo prima come funziona la memorizzazione nella cache.
Dovresti essere consapevole del fatto che quando avvii una nuova istanza del browser senza testa, Puppeteer crea una directory temporanea per il suo profilo. Viene rimosso quando il browser viene chiuso e non è disponibile per l'uso quando si avvia una nuova istanza, quindi tutte le immagini, CSS, cookie e altri oggetti archiviati non saranno più accessibili.
Possiamo forzare Puppeteer a utilizzare un percorso personalizzato per la memorizzazione di dati come cookie e cache, che verranno riutilizzati ogni volta che lo eseguiamo di nuovo, fino alla scadenza o all'eliminazione manuale.
const browser = await puppeteer.launch({ userDataDir: './data', });
Questo dovrebbe darci un bel salto di prestazioni, poiché molti CSS e immagini verranno memorizzati nella cache nella directory dei dati alla prima richiesta e Chrome non avrà bisogno di scaricarli ancora e ancora.
Tuttavia, tali risorse verranno comunque utilizzate durante il rendering della pagina. Nelle nostre esigenze di scraping degli articoli di notizie di Y Combinator, non dobbiamo davvero preoccuparci di elementi visivi, comprese le immagini. Ci preoccupiamo solo dell'output HTML nudo, quindi proviamo a bloccare ogni richiesta.
Fortunatamente, Puppeteer è piuttosto interessante con cui lavorare, in questo caso, perché viene fornito con il supporto per hook personalizzati. Possiamo fornire un intercettore su ogni richiesta e annullare quelle di cui non abbiamo davvero bisogno.
L'intercettore può essere definito nel modo seguente:
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });
Come puoi vedere, abbiamo il pieno controllo sulle richieste che vengono avviate. Possiamo scrivere una logica personalizzata per consentire o interrompere richieste specifiche in base al loro resourceType
. Abbiamo anche accesso a molti altri dati come request.url
, quindi possiamo bloccare solo URL specifici, se lo desideriamo.
Nell'esempio sopra, consentiamo solo alle richieste con il tipo di risorsa "document"
di passare attraverso il nostro filtro, il che significa che bloccheremo tutte le immagini, CSS e tutto il resto oltre alla risposta HTML originale.
Ecco il nostro codice finale:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
Stai al sicuro con i limiti di tariffa
I browser senza testa sono strumenti molto potenti. Sono in grado di eseguire quasi tutti i tipi di attività di automazione Web e Burattinaio lo rende ancora più semplice. Nonostante tutte le possibilità, dobbiamo rispettare i termini di servizio di un sito Web per assicurarci di non abusare del sistema.
Poiché questo aspetto è più legato all'architettura, non lo tratterò in modo approfondito in questo tutorial di Burattinaio. Detto questo, il modo più semplice per rallentare uno script Burattinaio è aggiungere un comando di sospensione ad esso:
js await page.waitFor(5000);
Questa istruzione costringerà lo script a dormire per cinque secondi (5000 ms). Puoi metterlo ovunque prima di browser.close()
.
Proprio come limitare l'uso di servizi di terze parti, ci sono molti altri modi più solidi per controllare l'utilizzo di Burattinaio. Un esempio potrebbe essere la creazione di un sistema di code con un numero limitato di lavoratori. Ogni volta che si desidera utilizzare Puppeteer, si inserisce una nuova attività nella coda, ma ci sarebbe solo un numero limitato di lavoratori in grado di lavorare sulle attività in essa contenute. Questa è una pratica abbastanza comune quando si ha a che fare con i limiti di velocità delle API di terze parti e può essere applicata anche allo scraping dei dati web di Puppeteer.
Il posto del burattinaio nel web in rapido movimento
In questo tutorial di Burattinaio, ho dimostrato le sue funzionalità di base come strumento di scraping web. Tuttavia, ha casi d'uso molto più ampi, tra cui test del browser senza testa, generazione di PDF e monitoraggio delle prestazioni, tra molti altri.
Le tecnologie Web stanno avanzando rapidamente. Alcuni siti Web dipendono così tanto dal rendering JavaScript che è diventato quasi impossibile eseguire semplici richieste HTTP per raschiarle o eseguire una sorta di automazione. Fortunatamente, i browser headless stanno diventando sempre più accessibili per gestire tutte le nostre esigenze di automazione, grazie a progetti come Puppeteer e ai fantastici team dietro di loro!