Web Scraping cu un browser fără cap: un tutorial pentru păpuși
Publicat: 2022-03-11În acest articol, vom vedea cât de ușor este să efectuați web scraping (automatizare web) cu metoda oarecum netradițională de a folosi un browser fără cap .
Ce este un browser fără cap și de ce este necesar?
În ultimii ani, web-ul a evoluat de la site-uri web simpliste construite cu HTML și CSS. Acum există mult mai multe aplicații web interactive cu interfețe de utilizare frumoase, care sunt adesea construite cu cadre precum Angular sau React. Cu alte cuvinte, în zilele noastre JavaScript guvernează web-ul, inclusiv aproape tot ceea ce interacționați pe site-uri web.
Pentru scopurile noastre, JavaScript este un limbaj pentru client. Serverul returnează fișiere JavaScript sau scripturi injectate într-un răspuns HTML, iar browserul le procesează. Acum, aceasta este o problemă dacă facem un fel de web scraping sau automatizare web, deoarece de cele mai multe ori, conținutul pe care ne-am dori să-l vedem sau să scrapem este de fapt redat de codul JavaScript și nu este accesibil din răspunsul HTML brut. pe care serverul le livrează.
După cum am menționat mai sus, browserele știu cum să proceseze JavaScript și să redeze pagini web frumoase. Acum, ce se întâmplă dacă am putea folosi această funcționalitate pentru nevoile noastre de scraping și am avea o modalitate de a controla browserele în mod programatic? Exact aici intervine automatizarea browserului fără cap!
Fără cap? Scuzati-ma? Da, asta înseamnă doar că nu există o interfață grafică cu utilizatorul (GUI). În loc să interacționați cu elementele vizuale așa cum ați face în mod normal, de exemplu cu un mouse sau un dispozitiv tactil, automatizați cazurile de utilizare cu o interfață de linie de comandă (CLI).
Chrome fără cap și păpușar
Există multe instrumente de scraping web care pot fi folosite pentru navigarea fără cap, cum ar fi Zombie.js sau Firefox fără cap folosind Selenium. Însă astăzi vom explora Chrome fără cap prin Puppeteer, deoarece este un player relativ mai nou, lansat la începutul anului 2018. Nota editorului: Merită menționat Intoli's Remote Browser, un alt player nou, dar acesta va trebui să fie un subiect pentru altul articol.
Ce este de fapt Marionetarul? Este o bibliotecă Node.js care oferă un API de nivel înalt pentru a controla Chrome sau Chromium fără cap sau pentru a interacționa cu protocolul DevTools. Este întreținut de echipa Chrome DevTools și de o comunitate minunată cu sursă deschisă.
Destul de vorbit — haideți să intrăm în cod și să explorăm lumea cum să automatizăm scrapingul web folosind navigarea fără cap a lui Puppeteer!
Pregătirea Mediului
În primul rând, va trebui să aveți instalat Node.js 8+ pe computer. Îl puteți instala aici sau dacă sunteți iubitor de CLI ca mine și vă place să lucrați pe Ubuntu, urmați aceste comenzi:
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
De asemenea, veți avea nevoie de câteva pachete care pot fi sau nu disponibile pe sistemul dumneavoastră. Doar pentru a fi în siguranță, încercați să instalați acelea:
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ți Headless Chrome și Puppeteer
Aș recomanda instalarea Puppeteer cu npm
, deoarece va include și versiunea stabilă actualizată de Chromium, care este garantat să funcționeze cu biblioteca.
Rulați această comandă în directorul rădăcină al proiectului:
npm i puppeteer --save
Notă: acest lucru poate dura ceva timp, deoarece Puppeteer va trebui să descarce și să instaleze Chromium în fundal.
Bine, acum că suntem pregătiți și configurați, să înceapă distracția!
Utilizarea API-ului Puppeteer pentru Web Scraping automat
Să începem tutorialul nostru Puppeteer cu un exemplu de bază. Vom scrie un script care va face ca browserul nostru fără cap să facă o captură de ecran a unui site web la alegerea noastră.
Creați un fișier nou în directorul de proiect numit screenshot.js
și deschideți-l în editorul de cod preferat.
Mai întâi, să importăm biblioteca Puppeteer în scriptul tău:
const puppeteer = require('puppeteer');
În continuare, să luăm adresa URL din argumentele liniei de comandă:
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }
Acum, trebuie să ținem cont de faptul că Puppeteer este o bibliotecă bazată pe promisiuni: efectuează apeluri asincrone către instanța Chrome fără cap de sub capotă. Să păstrăm codul curat utilizând async/wait. Pentru asta, trebuie să definim mai întâi o funcție async
și să punem tot codul Puppeteer acolo:
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();
În total, codul final arată astfel:
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();
Îl puteți rula executând următoarea comandă în directorul rădăcină al proiectului dvs.:
node screenshot.js https://github.com
Așteaptă o secundă și bum! Browserul nostru fără cap tocmai a creat un fișier numit screenshot.png
și puteți vedea pagina de pornire GitHub redată în el. Grozav, avem un răzuitor web Chrome funcțional!
Să ne oprim un minut și să explorăm ce se întâmplă în funcția noastră run()
mai sus.
Mai întâi, lansăm o nouă instanță de browser fără cap, apoi deschidem o nouă pagină (filă) și navigăm la adresa URL furnizată în argumentul liniei de comandă. În cele din urmă, folosim metoda încorporată a lui Puppeteer pentru a face o captură de ecran și trebuie doar să furnizăm calea în care ar trebui să fie salvată. De asemenea, trebuie să ne asigurăm că închidem browserul fără cap după ce am terminat cu automatizarea.
Acum că am acoperit elementele de bază, să trecem la ceva ceva mai complex.
Un al doilea exemplu de răzuire a păpușilor
Pentru următoarea parte a tutorialului nostru Puppeteer, să presupunem că vrem să eliminăm cele mai noi articole de la Hacker News.
Creați un fișier nou numit ycombinator-scraper.js
și inserați în următorul fragment de cod:
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);
Bine, se întâmplă ceva mai mult aici în comparație cu exemplul anterior.
Primul lucru pe care l-ați putea observa este că funcția run()
returnează acum o promisiune, astfel încât prefixul async
sa mutat la definiția funcției promise.
De asemenea, am împachetat tot codul nostru într-un bloc try-catch, astfel încât să putem gestiona orice erori care provoacă respingerea promisiunii noastre.
Și, în sfârșit, folosim metoda încorporată a lui Puppeteer numită evaluate()
. Această metodă ne permite să rulăm cod JavaScript personalizat ca și cum l-am executa în consola DevTools. Orice lucru returnat din acea funcție este rezolvat prin promisiune. Această metodă este foarte utilă atunci când vine vorba de răzuirea informațiilor sau de a efectua acțiuni personalizate.
Codul transmis metodei evaluate()
este JavaScript de bază, care construiește o serie de obiecte, fiecare având câmpuri url
și text
care reprezintă adresele URL ale poveștii pe care le vedem pe https://news.ycombinator.com/.
Ieșirea scriptului arată cam așa (dar cu 30 de intrări, inițial):
[ { 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' } ]
Destul de îngrijit, aș spune!
Bine, hai să mergem înainte. Am avut doar 30 de articole returnate, în timp ce sunt multe altele disponibile - sunt doar pe alte pagini. Trebuie să facem clic pe butonul „Mai multe” pentru a încărca următoarea pagină de rezultate.

Să ne modificăm puțin scriptul pentru a adăuga un suport pentru paginare:
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);
Să revedem ce am făcut aici:
- Am adăugat un singur argument numit
pagesToScrape
la funcția noastră principalărun()
. Vom folosi acest lucru pentru a limita câte pagini va răzui scriptul nostru. - Mai există o nouă variabilă numită
currentPage
, care reprezintă numărul paginii de rezultate la care ne uităm în prezent. Este setat la1
inițial. De asemenea, am împachetat funcțiaevaluate()
într-o buclăwhile
, astfel încât să continue să ruleze atâta timp câtcurrentPage
este mai mic sau egal cupagesToScrape
. - Am adăugat blocul pentru trecerea la o pagină nouă și așteptarea încărcării paginii înainte de a reporni bucla
while
.
Veți observa că am folosit metoda page.click()
pentru ca browserul fără cap să facă clic pe butonul „Mai multe”. De asemenea, am folosit metoda waitForSelector()
pentru a ne asigura că logica noastră este întreruptă până când conținutul paginii este încărcat.
Ambele sunt metode Puppeteer API de nivel înalt gata de utilizare.
Una dintre problemele pe care probabil le veți întâlni în timpul răzuirii cu Puppeteer este așteptarea încărcării unei pagini. Hacker News are o structură relativ simplă și a fost destul de ușor să așteptați finalizarea încărcării paginii. Pentru cazuri de utilizare mai complexe, Puppeteer oferă o gamă largă de funcționalități încorporate, pe care le puteți explora în documentația API de pe GitHub.
Toate acestea sunt destul de grozave, dar tutorialul nostru Puppeteer nu a acoperit încă optimizarea. Să vedem cum îl putem face pe Puppeteer să alerge mai repede.
Optimizarea scenariului de păpuși
Ideea generală este să nu lăsați browserul fără cap să facă vreo muncă suplimentară. Aceasta poate include încărcarea imaginilor, aplicarea regulilor CSS, lansarea solicitărilor XHR etc.
Ca și în cazul altor instrumente, optimizarea lui Puppeteer depinde de cazul exact de utilizare, așa că rețineți că unele dintre aceste idei ar putea să nu fie potrivite pentru proiectul dvs. De exemplu, dacă am fi evitat să încărcăm imagini în primul nostru exemplu, captura de ecran ar fi putut să nu arate așa cum ne-am dorit.
Oricum, aceste optimizări pot fi realizate fie prin memorarea în cache a activelor la prima solicitare, fie anularea solicitărilor HTTP pe măsură ce sunt inițiate de site-ul web.
Să vedem mai întâi cum funcționează memoria cache.
Ar trebui să știți că atunci când lansați o nouă instanță de browser fără cap, Puppeteer creează un director temporar pentru profilul său. Este eliminat când browserul este închis și nu este disponibil pentru utilizare atunci când porniți o instanță nouă - astfel toate imaginile, CSS-urile, cookie-urile și alte obiecte stocate nu vor mai fi accesibile.
Putem forța Puppeteer să folosească o cale personalizată pentru stocarea datelor precum cookie-urile și memoria cache, care vor fi reutilizate de fiecare dată când o rulăm din nou - până când expiră sau sunt șterse manual.
const browser = await puppeteer.launch({ userDataDir: './data', });
Acest lucru ar trebui să ne ofere o creștere bună în performanță, deoarece o mulțime de CSS și imagini vor fi stocate în cache în directorul de date la prima solicitare, iar Chrome nu va trebui să le descarce din nou și din nou.
Cu toate acestea, acele elemente vor fi utilizate în continuare la randarea paginii. În nevoile noastre de eliminare a articolelor de știri Y Combinator, nu trebuie să ne facem griji cu privire la nicio imagine, inclusiv imagini. Ne pasă doar de rezultatul HTML simplu, așa că hai să încercăm să blocăm fiecare solicitare.
Din fericire, Puppeteer este destul de grozav de lucrat, în acest caz, deoarece vine cu suport pentru cârlige personalizate. Putem oferi un interceptor pentru fiecare solicitare și le putem anula pe cele de care nu avem cu adevărat nevoie.
Interceptorul poate fi definit în felul următor:
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });
După cum puteți vedea, avem control deplin asupra solicitărilor care sunt inițiate. Putem scrie o logică personalizată pentru a permite sau anula cereri specifice în funcție de tipul lor de resourceType
. Avem, de asemenea, acces la o mulțime de alte date, cum ar fi request.url
, astfel încât să putem bloca numai anumite adrese URL dacă dorim.
În exemplul de mai sus, permitem doar cererilor cu tipul de resursă "document"
să treacă prin filtrul nostru, ceea ce înseamnă că vom bloca toate imaginile, CSS și orice altceva în afară de răspunsul HTML original.
Iată codul nostru final:
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);
Rămâneți în siguranță cu limite de rată
Browserele fără cap sunt instrumente foarte puternice. Ei sunt capabili să efectueze aproape orice fel de sarcină de automatizare web, iar Puppeteer face acest lucru și mai ușor. În ciuda tuturor posibilităților, trebuie să respectăm termenii și condițiile unui site web pentru a ne asigura că nu abuzăm de sistem.
Deoarece acest aspect este mai mult legat de arhitectură, nu voi trata acest lucru în profunzime în acest tutorial Puppeteer. Acestea fiind spuse, cel mai simplu mod de a încetini un script Puppeteer este să îi adăugați o comandă de repaus:
js await page.waitFor(5000);
Această declarație va forța scriptul să intre timp de cinci secunde (5000 ms). Puteți pune acest lucru oriunde înainte de browser.close()
.
La fel ca limitarea utilizării serviciilor de la terți, există o mulțime de alte modalități mai solide de a controla utilizarea Puppeteer. Un exemplu ar fi construirea unui sistem de cozi cu un număr limitat de lucrători. De fiecare dată când doriți să utilizați Puppeteer, ați împinge o nouă sarcină în coadă, dar ar exista doar un număr limitat de lucrători capabili să lucreze la sarcinile din ea. Aceasta este o practică destul de comună atunci când aveți de-a face cu limitele de rată a API-ului terță parte și poate fi aplicată și la scraping-ul datelor web Puppeteer.
Locul păpușarului în rețeaua în mișcare rapidă
În acest tutorial Puppeteer, am demonstrat funcționalitatea sa de bază ca instrument de răzuire web. Cu toate acestea, are cazuri de utilizare mult mai largi, inclusiv testarea browserului fără cap, generarea PDF și monitorizarea performanței, printre multe altele.
Tehnologiile web avansează rapid. Unele site-uri web sunt atât de dependente de redarea JavaScript, încât a devenit aproape imposibil să executați solicitări HTTP simple pentru a le elimina sau a efectua un fel de automatizare. Din fericire, browserele fără cap devin din ce în ce mai accesibile pentru a face față tuturor nevoilor noastre de automatizare, datorită proiectelor precum Puppeteer și echipelor minunate din spatele lor!