Web Scraping und Parsen von HTML mit Node.js und Cheerio

February 07, 2020
Autor:in:
Sam Agnew
Twilion

Web Scraping und Parsen von HTML mit Node.js und Cheerio

Hallo und Danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von Web Scraping and Parsing HTML with Node.js and Cheerio. Während wir unsere Übersetzungsprozesse verbessern, würden wir uns über Dein Feedback an help@twilio.com freuen, solltest Du etwas bemerken, was falsch übersetzt wurde. Wir bedanken uns für hilfreiche Beiträge mit Twilio Swag :)

Im Internet findet sich eine große Vielfalt an Daten, die wir nach Belieben verwenden können. Der programmgesteuerte Zugriff auf diese Daten ist allerdings oft schwierig, sofern er nicht über eine dedizierte REST API bereitgestellt wird. Aber mit einem Node.js-Tool wie Cheerio können wir diese Daten direkt aus den Webseiten scrapen und parsen, um sie für unsere Projekte und Anwendungen zu nutzen.

Ein Beispiel dafür wäre das Scraping von MIDI-Daten, um ein neuronales Netzwerk so zu trainieren, dass es Musik im klassischen Nintendo-Stil generiert. Dazu brauchen wir zunächst Dateien mit Musik aus alten Nintendo-Spielen. Mit Cheerio können wir uns diese Daten aus dem Video Game Music Archive holen.

Vorbereitende Aufgaben und Einrichten von Abhängigkeiten

Als Erstes müssen wir eine aktuelle Version von Node.js und npm installieren.

Wir navigieren zu dem Verzeichnis, in dem wir den Code speichern möchten, und führen den folgenden Befehl auf dem Terminal aus, um ein Paket für dieses Projekt zu erstellen:

npm init --yes

Das Argument --yes durchläuft alle Eingabeaufforderungen, die wir ansonsten ausfüllen oder überspringen müssten. Wir haben jetzt eine package.json-Datei für unsere App.

Wir benötigen die Got-Bibliothek, um HTTP-Anfragen zum Abrufen von Daten von der Webseite durchzuführen. Das HTML parsen wir mit Cheerio.

Wir führen den folgenden Befehl auf dem Terminal aus, um diese Bibliotheken zu installieren:

npm install got@10.4.0 cheerio@1.0.0-rc.3

Cheerio implementiert ein Subset des Kerns von jQuery – daher werden die Funktionen dieses Tools vielen JavaScript-Entwicklern bekannt vorkommen. Wir sehen uns jetzt genauer an, wie jsdom funktioniert.

Mithilfe von Got Daten zur Verwendung in Cheerio abrufen

Als Erstes schreiben wir Code, um das HTML der Webseite zu erfassen und zu prüfen, wie wir das Parsing starten. Mit dem folgenden Code senden wir eine GET-Anfrage an die gewünschte Webseite und erstellen ein Cheerio-Objekt mit dem HTML der Seite. Wir nennen es $ – gemäß der berühmt-berüchtigten jQuery-Namenskonvention:

const fs = require('fs');
const cheerio = require('cheerio');
const got = require('got');

const vgmUrl= 'https://www.vgmusic.com/music/console/nintendo/nes';

got(vgmUrl).then(response => {
  const $ = cheerio.load(response.body);
  console.log($('title')[0]);
}).catch(err => {
  console.log(err);
});

Mit diesem $-Objekt navigieren wir durch das HTML und rufen die DOM-Elemente für die gewünschten Daten ab, genauso wie mit jQuery. Wenn wir beispielsweise $('title') eingeben, erhalten wir ein Array von Objekten, die jedem <title>-Tag auf der Seite entsprechen. In der Regel gibt es nur ein title-Element, das heißt, unser Array enthält nur ein Objekt. Wenn wir diesen Code mit dem Befehl node index.js ausführen, protokolliert er die Struktur des Objekts auf der Konsole.

Mit Cheerio vertraut machen

Mit einem Objekt, das einem Element im geparsten HTML entspricht, können wir beispielsweise durch seine untergeordneten, übergeordneten und gleichgeordneten Elemente navigieren. Das untergeordnete Element dieses <title>-Elements ist der Text zwischen den Tags. Über den Befehl console.log($('title')[0].children[0].data); protokollieren wir also den Titel der Webseite.

Wenn wir eine spezifischere Abfrage stellen wollen, können wir eine Vielzahl von Selektoren nutzen, um durch das HTML zu parsen. Zu den beiden gängigsten Selektoren gehören die Suche nach Class (Klasse) oder ID. Um ein DIV (also einen Bereich) mit der ID „menu“ zu finden, würden wir den Befehl $('#menu') ausführen, und um alle Spalten aus der Tabelle der VGM MIDIs mit der Klasse „header“ zu erhalten, müsste der Befehl $('td.header') lauten.

Auf dieser Seite wollen wir uns die Hyperlinks zu allen MIDI-Dateien holen, die wir herunterladen möchten. Wir geben $('a') ein, um jeden Link auf der Seite abzurufen. Wir fügen dem Code in index.js Folgendes hinzu:


got(vgmUrl).then(response => {
  const $ = cheerio.load(response.body);

  $('a').each((i, link) => {
    const href = link.attribs.href;
    console.log(href);
  });
}).catch(err => {
  console.log(err);
});

Dieser Code protokolliert die URL für jeden Link auf der Seite. Wir sehen, dass wir mit der Funktion .each() durch alle Elemente eines bestimmten Selektors schalten können. So schön es ist, dass wir nun durch jeden Link auf der Seite iterieren können – um alle MIDI-Dateien herunterladen zu können, müssen wir uns genauer ausdrücken.

Mit Cheerio die HTML-Elemente filtern

Bevor wir weiteren Code zum Parsen des gewünschten Content schreiben, sehen wir uns das vom Browser gerenderte HTML an. Jede Webseite ist anders – um die richtigen Daten zurückzuerhalten, ist daher ein gewisses Maß an Kreativität, Mustererkennung und Experimentierfreudigkeit gefragt.

MIDI-Dateien auf der Website

Unser Ziel besteht darin, eine Reihe von MIDI-Dateien herunterzuladen. Allerdings gibt es auf dieser Webseite zahlreiche doppelte Tracks sowie Remixes der Songs. Wir brauchen nur jeweils eine Version jedes Songs. Und da wir letztendlich mit den betreffenden Daten ein neuronales Netzwerk so trainieren wollen, dass es stilechte Nintendo-Musik generiert, ist es nicht sinnvoll, benutzerspezifische Remixes für die Konfiguration zu verwenden.

Wer Code zum Parsen einer Webseite schreibt, sollte sich auch die Entwicklertools zunutze machen, die in den meisten modernen Browsern verfügbar sind. Wenn wir mit der rechten Maustaste auf das gewünschte Element klicken, können wir den HTML-Code prüfen, der dem Element zugrunde liegt. So verschaffen wir uns einen genaueren Einblick.

Dev-Tools

Cheerio bietet die Möglichkeit, Filterfunktionen zu erstellen, um genauer festzulegen, welche Daten unsere Selektoren zurückgeben sollen. Diese Funktionen durchschleifen alle Elemente eines bestimmten Selektors und geben als Antwort „true“ (wahr) oder „false“ (falsch) zurück, je nachdem, ob sie im Set berücksichtigt werden sollten oder nicht.

Wer sich die im vorigen Schritt protokollierten Daten genauer angesehen hat, dem ist möglicherweise aufgefallen, dass es zahlreiche Links auf der Seite gibt, die kein href-Attribut haben und daher nirgendwohin führen. Da dies ganz sicher nicht die von uns gesuchten MIDIs sind, schreiben wir eine kurze Funktion, um diese Links rauszufiltern und um sicherzustellen, dass die Links mit einem href-Element auch tatsächlich zu einer .mid-Datei führen:

const isMidi = (i, link) => {
  // Return false if there is no href attribute.
  if(typeof link.attribs.href === 'undefined') { return false }

  return link.attribs.href.includes('.mid');
};

Die nächste Herausforderung ist, dass wir weder Duplikate noch benutzergenerierte Remixes herunterladen wollen. Wir erreichen dies mit regulären Ausdrücken. Diese sorgen dafür, dass wir nur Links mit Text ohne Klammern erhalten – schließlich enthalten nur die Duplikate und Remixes Klammern:

const noParens = (i, link) => {
  // Regular expression to determine if the text has parentheses.
  const parensRegex = /^((?!\().)*$/;
  return parensRegex.test(link.children[0].data);
};

Wir versuchen, diese dem Code in index.js hinzuzufügen:


got(vgmUrl).then(response => {
  const $ = cheerio.load(response.body);

  $('a').filter(isMidi).filter(noParens).each((i, link) => {
    const href = link.attribs.href;
    console.log(href);
  });
});

Wir führen diesen Code erneut aus. Es sollten jetzt nur .mid-Dateien gedruckt werden.

Herunterladen der gewünschten MIDI-Dateien von der Webseite

Wir haben also jetzt den nötigen Code, um durch jede der gewünschten MIDI-Dateien zu iterieren. Nun brauchen wir Code, um alle dieser Dateien herunterzuladen.

In der Callback-Funktion für das Durchschleifen aller MIDI-Links fügen wir diesen Code hinzu. Der MIDI-Download wird dann in eine lokale Datei gestreamt, inklusive Fehlerprüfung:


  $('a').filter(isMidi).filter(noParens).each((i, link) => {
    const fileName = link.attribs.href;

    got.stream(`${vgmUrl}/${fileName}`)
      .on('error', err => { console.log(err); console.log(`Error on ${vgmUrl}/${fileName}`) })
      .pipe(fs.createWriteStream(`MIDIs/${fileName}`))
      .on('error', err => { console.log(err); console.log(`Error on ${vgmUrl}/${fileName}`) })
      .on('finish', () => console.log(`Finished ${fileName}`));
  });

Wir führen diesen Code von dem Verzeichnis aus, in dem wir alle MIDI-Dateien speichern möchten. Wir sollten auf dem Terminal-Bildschirm sehen, dass alle 2230 MIDI-Dateien heruntergeladen wurden (Zahl zum Zeitpunkt der Erstellung dieses Blogs). Und damit haben wir das Scraping aller erforderlichen MIDI-Dateien abgeschlossen.

Terminal-Ausgabe

Jetzt können wir durch die Liste gehen, die Songs anhören und die Nintendo-Musik genießen!

Die endlose Weite des World Wide Web

Wir wissen nun, wie man programmgesteuert Inhalte aus Webseiten extrahieren kann, das heißt, wir haben jetzt Zugang zu einer gigantischen Datenquelle mit allem, was wir für unsere Projekte brauchen. Zu beachten ist, dass jegliche Änderungen am HTML einer Webseite zu Fehlern in unserem Code führen können. Daher ist es wichtig, dass wir alles auf dem neuesten Stand halten, wenn wir Anwendungen auf der Basis dieses Codes entwerfen. Es könnte außerdem hilfreich sein, die Funktionen der jsdom-Bibliothek mit anderen Lösungen zu vergleichen. Wir können uns dazu die Tutorials zu den Themen Web Scraping mit jsdom und Headless-Browser-Scripting mit Puppeteer bzw. einer ähnlichen Bibliothek namens Playwright ansehen.

Wie könnten wir die aus dem Video Game Music Archive extrahierten Daten nun weiterverwenden? Mit einer Python-Bibliothek wie Magenta können wir beispielsweise ein neuronales Netzwerk damit trainieren.

Ich bin gespannt auf eure Ergebnisse. Ihr könnt mich gerne kontaktieren, um eure Erfahrungen zu teilen oder Fragen zu stellen.