Web Scraping mit einem Headless Browser: Ein Puppeteer-Tutorial

Veröffentlicht: 2022-03-11

In diesem Artikel werden wir sehen, wie einfach es ist, Web Scraping (Web-Automatisierung) mit der etwas unkonventionellen Methode der Verwendung eines Headless-Browsers durchzuführen.

Was ist ein Headless Browser und warum wird er benötigt?

In den letzten Jahren hat sich das Web von einfachen Websites entwickelt, die mit reinem HTML und CSS erstellt wurden. Mittlerweile gibt es viel mehr interaktive Web-Apps mit schönen UIs, die oft mit Frameworks wie Angular oder React gebaut werden. Mit anderen Worten: Heutzutage regiert JavaScript das Web, einschließlich fast aller Dinge, mit denen Sie auf Websites interagieren.

Für unsere Zwecke ist JavaScript eine clientseitige Sprache. Der Server gibt JavaScript-Dateien oder Skripts zurück, die in eine HTML-Antwort eingefügt werden, und der Browser verarbeitet sie. Nun, dies ist ein Problem, wenn wir eine Art Web-Scraping oder Web-Automatisierung durchführen, da der Inhalt, den wir sehen oder kratzen möchten, in den meisten Fällen tatsächlich durch JavaScript-Code gerendert wird und nicht über die rohe HTML-Antwort zugänglich ist die der Server liefert.

Wie wir oben erwähnt haben, wissen Browser, wie sie JavaScript verarbeiten und schöne Webseiten rendern. Was wäre nun, wenn wir diese Funktionalität für unsere Scraping-Anforderungen nutzen könnten und eine Möglichkeit hätten, Browser programmgesteuert zu steuern? Genau hier setzt die Headless-Browser-Automatisierung an!

Kopflos? Verzeihung? Ja, das bedeutet nur, dass es keine grafische Benutzeroberfläche (GUI) gibt. Anstatt wie gewohnt mit visuellen Elementen zu interagieren – beispielsweise mit einer Maus oder einem Touch-Gerät – automatisieren Sie Anwendungsfälle mit einer Befehlszeilenschnittstelle (CLI).

Headless Chrome und Puppenspieler

Es gibt viele Web-Scraping-Tools, die für das Headless-Browsing verwendet werden können, wie Zombie.js oder Headless Firefox mit Selenium. Aber heute werden wir Headless Chrome über Puppeteer erkunden, da es sich um einen relativ neueren Player handelt, der Anfang 2018 veröffentlicht wurde. Anmerkung der Redaktion: Es lohnt sich, Intoli’s Remote Browser zu erwähnen, einen weiteren neuen Player, aber das muss ein Thema für einen anderen sein Artikel.

Was genau ist Puppenspieler? Es handelt sich um eine Node.js-Bibliothek, die eine High-Level-API bereitstellt, um Headless Chrome oder Chromium zu steuern oder mit dem DevTools-Protokoll zu interagieren. Es wird vom Chrome DevTools-Team und einer großartigen Open-Source-Community gepflegt.

Genug geredet – lassen Sie uns in den Code springen und die Welt der Automatisierung von Web Scraping mit Puppeteers Headless Browsing erkunden!

Umgebung vorbereiten

Zunächst muss Node.js 8+ auf Ihrem Computer installiert sein. Sie können es hier installieren, oder wenn Sie CLI-Liebhaber wie ich sind und gerne mit Ubuntu arbeiten, folgen Sie diesen Befehlen:

 curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs

Sie benötigen auch einige Pakete, die möglicherweise auf Ihrem System verfügbar sind oder nicht. Versuchen Sie zur Sicherheit, diese zu installieren:

 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

Richten Sie Headless Chrome und Puppeteer ein

Ich würde empfehlen, Puppeteer mit npm zu installieren, da es auch die stabile, aktuelle Chromium-Version enthält, die garantiert mit der Bibliothek funktioniert.

Führen Sie diesen Befehl in Ihrem Projektstammverzeichnis aus:

 npm i puppeteer --save

Hinweis: Dies kann eine Weile dauern, da Puppeteer Chromium im Hintergrund herunterladen und installieren muss.

Okay, jetzt, da wir alle eingerichtet und konfiguriert sind, kann der Spaß beginnen!

Verwenden der Puppeteer-API für automatisiertes Web Scraping

Beginnen wir unser Puppeteer-Tutorial mit einem einfachen Beispiel. Wir schreiben ein Skript, das unseren Headless-Browser veranlasst, einen Screenshot einer Website unserer Wahl zu machen.

Erstellen Sie in Ihrem Projektverzeichnis eine neue Datei mit dem Namen screenshot.js und öffnen Sie sie in Ihrem bevorzugten Code-Editor.

Lassen Sie uns zunächst die Puppeteer-Bibliothek in Ihr Skript importieren:

 const puppeteer = require('puppeteer');

Als nächstes nehmen wir die URL aus Befehlszeilenargumenten:

 const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }

Nun müssen wir bedenken, dass Puppeteer eine Promise-basierte Bibliothek ist: Sie führt asynchrone Aufrufe an die Headless-Chrome-Instanz unter der Haube durch. Lassen Sie uns den Code sauber halten, indem Sie async/await verwenden. Dazu müssen wir zuerst eine async Funktion definieren und den gesamten Puppeteer-Code dort einfügen:

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

Insgesamt sieht der endgültige Code so aus:

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

Sie können es ausführen, indem Sie den folgenden Befehl im Stammverzeichnis Ihres Projekts ausführen:

 node screenshot.js https://github.com

Warte eine Sekunde, und boom! Unser Headless-Browser hat gerade eine Datei namens screenshot.png erstellt und Sie können die darin gerenderte GitHub-Startseite sehen. Großartig, wir haben einen funktionierenden Chrome Web Scraper!

Lassen Sie uns für eine Minute innehalten und untersuchen, was in unserer Funktion run() oben passiert.

Zuerst starten wir eine neue Headless-Browser-Instanz, dann öffnen wir eine neue Seite (Tab) und navigieren zu der URL, die im Befehlszeilenargument angegeben ist. Schließlich verwenden wir die eingebaute Methode von Puppeteer, um einen Screenshot zu machen, und wir müssen nur den Pfad angeben, wo er gespeichert werden soll. Wir müssen auch sicherstellen, dass der Headless-Browser geschlossen wird, nachdem wir mit unserer Automatisierung fertig sind.

Nachdem wir nun die Grundlagen behandelt haben, gehen wir zu etwas Komplexerem über.

Ein zweites Beispiel für das Schaben eines Puppenspielers

Nehmen wir für den nächsten Teil unseres Puppeteer-Tutorials an, dass wir die neuesten Artikel von Hacker News zusammenkratzen möchten.

Erstellen Sie eine neue Datei namens ycombinator-scraper.js und fügen Sie das folgende Code-Snippet ein:

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

Okay, hier passiert ein bisschen mehr als im vorherigen Beispiel.

Das Erste, was Ihnen auffallen könnte, ist, dass die run() Funktion jetzt ein Promise zurückgibt, sodass das async -Präfix in die Definition der Promise-Funktion verschoben wurde.

Außerdem haben wir unseren gesamten Code in einen Try-Catch-Block verpackt, damit wir alle Fehler behandeln können, die dazu führen, dass unser Versprechen abgelehnt wird.

Und schließlich verwenden wir die eingebaute Methode von Puppeteer evaluate() . Mit dieser Methode können wir benutzerdefinierten JavaScript-Code so ausführen, als würden wir ihn in der DevTools-Konsole ausführen. Alles, was von dieser Funktion zurückgegeben wird, wird durch das Promise aufgelöst. Diese Methode ist sehr praktisch, wenn es darum geht, Informationen zu kratzen oder benutzerdefinierte Aktionen durchzuführen.

Der an die Methode „ evaluate() “ übergebene Code ist ziemlich einfaches JavaScript, das ein Array von Objekten erstellt, die jeweils url und text haben, die die Story-URLs darstellen, die wir auf https://news.ycombinator.com/ sehen.

Die Ausgabe des Skripts sieht ungefähr so ​​​​aus (aber mit ursprünglich 30 Einträgen):

 [ { 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' } ]

Ziemlich ordentlich, würde ich sagen!

Okay, gehen wir weiter. Wir haben nur 30 Artikel zurückgesendet bekommen, während noch viel mehr verfügbar sind – sie befinden sich nur auf anderen Seiten. Wir müssen auf die Schaltfläche „Mehr“ klicken, um die nächste Ergebnisseite zu laden.

Ändern wir unser Skript ein wenig, um eine Unterstützung für die Paginierung hinzuzufügen:

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

Sehen wir uns an, was wir hier gemacht haben:

  1. Wir haben ein einzelnes Argument namens pagesToScrape zu unserer Hauptfunktion run() hinzugefügt. Wir werden dies verwenden, um zu begrenzen, wie viele Seiten unser Skript kratzen wird.
  2. Es gibt eine weitere neue Variable namens currentPage , die die Nummer der Ergebnisseite darstellt, die wir gerade betrachten. Es ist anfangs auf 1 gesetzt. Außerdem haben wir unsere evaluate() Funktion in eine while -Schleife eingeschlossen, sodass sie so lange ausgeführt wird, wie currentPage kleiner oder gleich pagesToScrape ist.
  3. Wir haben den Block zum Wechseln zu einer neuen Seite und zum Warten auf das Laden der Seite hinzugefügt, bevor wir die while -Schleife neu starten.

Sie werden feststellen, dass wir die Methode page.click() verwendet haben, damit der Headless-Browser auf die Schaltfläche „Mehr“ klickt. Wir haben auch die Methode waitForSelector() verwendet, um sicherzustellen, dass unsere Logik angehalten wird, bis die Seiteninhalte geladen sind.

Beides sind hochrangige Puppeteer-API-Methoden, die sofort einsatzbereit sind.

Eines der Probleme, auf die Sie wahrscheinlich beim Scraping mit Puppeteer stoßen werden, ist das Warten auf das Laden einer Seite. Hacker News hat eine relativ einfache Struktur und es war ziemlich einfach, auf den Abschluss des Ladens der Seite zu warten. Für komplexere Anwendungsfälle bietet Puppeteer eine breite Palette integrierter Funktionen, die Sie in der API-Dokumentation auf GitHub erkunden können.

Das ist alles ziemlich cool, aber unser Puppeteer-Tutorial hat die Optimierung noch nicht behandelt. Mal sehen, wie wir Puppeteer schneller laufen lassen können.

Optimierung unseres Puppeteer-Skripts

Die allgemeine Idee ist, den Headless-Browser keine zusätzliche Arbeit erledigen zu lassen. Dies kann das Laden von Bildern, das Anwenden von CSS-Regeln, das Auslösen von XHR-Anforderungen usw. umfassen.

Wie bei anderen Tools hängt die Optimierung von Puppeteer vom genauen Anwendungsfall ab. Denken Sie also daran, dass einige dieser Ideen möglicherweise nicht für Ihr Projekt geeignet sind. Wenn wir beispielsweise in unserem ersten Beispiel das Laden von Bildern vermieden hätten, hätte unser Screenshot möglicherweise nicht so ausgesehen, wie wir es wollten.

Wie auch immer, diese Optimierungen können entweder durch Zwischenspeichern der Assets bei der ersten Anfrage oder durch vollständiges Abbrechen der HTTP-Anfragen, wenn sie von der Website initiiert werden, erreicht werden.

Sehen wir uns zuerst an, wie das Caching funktioniert.

Sie sollten sich darüber im Klaren sein, dass Puppeteer beim Starten einer neuen Headless-Browser-Instanz ein temporäres Verzeichnis für sein Profil erstellt. Es wird entfernt, wenn der Browser geschlossen wird, und steht nicht zur Verfügung, wenn Sie eine neue Instanz starten – daher sind alle Bilder, CSS, Cookies und andere gespeicherte Objekte nicht mehr zugänglich.

Wir können Puppeteer zwingen, einen benutzerdefinierten Pfad zum Speichern von Daten wie Cookies und Cache zu verwenden, die bei jeder erneuten Ausführung wiederverwendet werden – bis sie ablaufen oder manuell gelöscht werden.

 const browser = await puppeteer.launch({ userDataDir: './data', });

Dies sollte uns eine schöne Leistungssteigerung verschaffen, da viele CSS und Bilder bei der ersten Anfrage im Datenverzeichnis zwischengespeichert werden und Chrome sie nicht immer wieder herunterladen muss.

Diese Assets werden jedoch weiterhin beim Rendern der Seite verwendet. In unserem Scraping-Bedarf an Y Combinator-Nachrichtenartikeln müssen wir uns nicht wirklich um visuelle Elemente kümmern, einschließlich der Bilder. Wir kümmern uns nur um die reine HTML-Ausgabe, also versuchen wir, jede Anfrage zu blockieren.

Glücklicherweise ist es in diesem Fall ziemlich cool, mit Puppeteer zu arbeiten, da es Unterstützung für benutzerdefinierte Hooks bietet. Wir können für jede Anfrage einen Interceptor bereitstellen und diejenigen stornieren, die wir nicht wirklich benötigen.

Der Abfangjäger kann wie folgt definiert werden:

 await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });

Wie Sie sehen können, haben wir die volle Kontrolle über die initiierten Anfragen. Wir können benutzerdefinierte Logik schreiben, um bestimmte Anforderungen basierend auf ihrem resourceType zuzulassen oder abzubrechen. Wir haben auch Zugriff auf viele andere Daten wie request.url , sodass wir nur bestimmte URLs blockieren können, wenn wir dies wünschen.

Im obigen Beispiel lassen wir nur Anfragen mit dem Ressourcentyp "document" durch unseren Filter, was bedeutet, dass wir alle Bilder, CSS und alles andere außer der ursprünglichen HTML-Antwort blockieren.

Hier ist unser endgültiger Code:

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

Bleiben Sie sicher mit Ratenbegrenzungen

Headless-Browser sind sehr mächtige Tools. Sie können fast jede Art von Web-Automatisierungsaufgabe ausführen, und Puppeteer macht dies noch einfacher. Trotz aller Möglichkeiten müssen wir die Nutzungsbedingungen einer Website einhalten, um sicherzustellen, dass wir das System nicht missbrauchen.

Da dieser Aspekt eher architekturbezogen ist, werde ich ihn in diesem Puppeteer-Tutorial nicht ausführlich behandeln. Die einfachste Möglichkeit, ein Puppeteer-Skript zu verlangsamen, besteht darin, ihm einen Schlafbefehl hinzuzufügen:

js await page.waitFor(5000);

Diese Anweisung zwingt Ihr Skript für fünf Sekunden (5000 ms) in den Ruhezustand. Sie können dies an einer beliebigen Stelle vor browser.close() .

Genau wie die Einschränkung Ihrer Nutzung von Diensten von Drittanbietern gibt es viele andere robustere Möglichkeiten, Ihre Nutzung von Puppeteer zu kontrollieren. Ein Beispiel wäre der Aufbau eines Warteschlangensystems mit einer begrenzten Anzahl von Arbeitern. Jedes Mal, wenn Sie Puppeteer verwenden möchten, würden Sie eine neue Aufgabe in die Warteschlange schieben, aber es wäre nur eine begrenzte Anzahl von Arbeitern in der Lage, an den darin enthaltenen Aufgaben zu arbeiten. Dies ist eine ziemlich gängige Praxis beim Umgang mit API-Ratenbegrenzungen von Drittanbietern und kann auch auf Puppeteer-Webdaten-Scraping angewendet werden.

Der Platz des Puppenspielers im schnelllebigen Web

In diesem Puppeteer-Tutorial habe ich seine grundlegende Funktionalität als Web-Scraping-Tool demonstriert. Es hat jedoch viel breitere Anwendungsfälle, darunter Headless-Browser-Tests, PDF-Generierung und Leistungsüberwachung, unter vielen anderen.

Web-Technologien entwickeln sich schnell weiter. Einige Websites sind so abhängig von JavaScript-Rendering, dass es fast unmöglich geworden ist, einfache HTTP-Anfragen auszuführen, um sie zu kratzen oder irgendeine Art von Automatisierung durchzuführen. Glücklicherweise werden Headless-Browser dank Projekten wie Puppeteer und den großartigen Teams dahinter immer zugänglicher, um all unsere Automatisierungsanforderungen zu erfüllen!