Scrapy Tutorial – High-Level Web-Crawling-Framework (Python)

Scrapy ist ein Anwendungsframework zum Crawlen von Websites und Extrahieren strukturierter Daten.

Neben klassischem Web-Scraping kann das Framework auch für das Extrahieren von Daten über APIs verwendet werden.

Es ist zu empfehlen die Installation von Scrapy in einer virtuellen Umgebung vorzunehmen (siehe offizieller Installations-Guide).

In meinem Tutorial werden wir meine Seite educado.io crawlen. Es ist ein altes Demo-Projekt von mir und ist mittlerweile nicht mehr aktiv.

Virtual Environment/PIP Scrapy

Es wird empfohlen, für jedes Scrapy-Projekt eine eigene virtuelle Umgebung zu erstellen. Eine Möglichkeit, eine virtuelle Umgebung zu erstellen, ist venv, das in Python enthalten ist.

Zuerst sollte man in dem Terminal zu dem Ordner navigieren indem man das ganze Projekt abspeichern will und dann folgendes eingeben:

py -m venv virtual

Dadurch wird eine virtuelle Umgebung eingerichtet und ein Ordner namens “virtual” mit Unterordnern und Dateien erstellt:

Aktiviert wird die virtuelle Umgebung im Terminal folgendermaßen:

virtual\Scripts\activate.bat

Dann noch Scrapy auf der virtuellen Umgebung installieren und es kann losgehen:

Einrichtung eines neuen Projektes

Jedes Scrapy Projekt beginnt mit indem man in die Konsole scrapy startproject <beliebigerName> eingibt. Wir nennen unser Projekt Mila:

Jetzt habt ihr in eurem vorher festgelegten Ordner folgende neue Dateien:

Ebene 1
Ebene 2

Ebene 1:

scrapy.cfg: Konfigurationsdatei

Ebene 2:

Spider-Ordner: Der Ordner wo ihr später eure Crawler reinpackt

items.py: Definitionsdatei für Projektelemente

pipelines.py: Für Projekt-Pipelines

settings.py: Für Einstellungen

Erster Crawler

Spider (Crawler) sind Klassen, die wir definieren und die Scrapy verwendet, um Informationen von einer Website zu scrapen. Sie müssen die Unterklasse Spider bilden, optional wie man Links auf den Seiten folgt und wie man den heruntergeladenen Seiteninhalt analysiert, um Daten zu extrahieren.

Dies ist der Code für unseren ersten Crawler. Speichern Sie ihn in einer Datei namens teacher_spider.py im Verzeichnis mila/spiders in Ihrem Projekt:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "teacher"

    def start_requests(self):
        urls = [
            'https://educado.io/seo/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = f'teacher-{page}.html'
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log(f'Saved file {filename}')

Um unseren ersten Crawl auszuführen müssen wir (wieder über die Konsole) in unser Projekt reinnavigieren um dann folgenden Befehl zu nutzen:

scrapy crawl teacher

Bevor wir weiter machen schauen wir uns jetzt zuerst einmal den Code an, den wir als teacher_spider.py gespeichert haben.

name: Hiermit legen wir den Namen des Crawlers fest. Diesen brauchen wir um ihn zum Beispiel in der Konsole aufrufen zu können. Das haben wir mit scrapy crawl teacher bereits getan.

Der Name muss innerhalb eines Projekts eindeutig sein, darf also nich mehrfach vorkommen.

start_requests(): Hier legen wir fest welche URLs wir crawlen wollen. Das kann wie in unserem ersten Beispiel anhand einer konkreten URL passieren oder mit Hilfe einer vorher generierten Liste oder Generatorfunktionen etc.

parse(): mit der Parse-Methode wird festgelegt, was mit den gecrawlten URLs passiert. In unserem Fall wird die Seite komplett als HTML Dokument in unseren Ordner gespeichert.

Extrahieren von Daten in unserem Crawler

Bis jetzt extrahiert unser Crawler keine besonderen Daten, sondern speichert lediglich die gesamte HTML-Seite in einer lokalen Datei.

Ein Scrapy-Spider erzeugt typischerweise viele Dictionaries, die die aus der Seite extrahierten Daten enthalten. Dazu verwenden wir das Python-Schlüsselwort yield. Vorher aber noch etwas anderes:

In der folgenden Query gehen wir wieder in unsere Datei teacher_spider.py und vereinfachen zuerst die Art und Weise, wie wir URLs abfragen. Anstatt eine start_requests()-Methode zu implementieren, die scrapy.Request-Objekte aus URLs erzeugt, können Sie einfach ein start_urls-Klassenattribut mit einer Liste von URLs definieren. Diese Liste wird dann von der Standardimplementierung von start_requests() verwendet.

Wir ersetzen also

class QuotesSpider(scrapy.Spider):
    name = "teacher"

    def start_requests(self):
        urls = [
            'https://educado.io/seo/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

durch:

class QuotesSpider(scrapy.Spider):
    name = "teacher"
    start_urls = [
        'https://educado.io/seo/',
    ]

Was wir jetzt wollen ist, von jedem Dozenten auf educado.io/seo/, folgende Informationen extrahieren und abspeichern:

  • Namen
  • Rating
  • Stundensatz
  • Spezialisierungen

Für das extrahieren, müssen wir die Parse Funktion überschreiben. Bevor wir aber die von uns gewünschten Informationen überschreiben, werden wir uns erstmal an eine einfachere Information heranwagen: Die Überschrift:

Um jetzt die Überschrift aus der Website herauszuparsen müssen wir uns erstmal den den html-Code anschauen:

Wir nehmen uns jetzt einfach den Codeausschnitt um die Überschrift drum herum (ob man jetzt einen Tag mehr nimmt der nicht ist egal, wenn man das Prinzip irgendwann raushat):

Bevor wir uns nochmal dem html-Snippet widmen gehen wir jetzt erstmal in die Grundstruktur der Parse-Funktion, die wir jetzt bauen:

Schauen wir uns zuerst response.css(‘tag.class’) an. In der Regel wollen wir mehrere Attribute parsen, also zum Beispiel Namen, Stundensatz, Anzahl Stunden usw.

Die Informationen sind vom Code her in irgendwelchen Tags. Ein Tag kann zum Beispiel ein <div>-Element sein oder auch ein <p>-Tag. Um das zu verstehen ist ein klein wenig Wissen wie ein HTML aufgebaut ist hilfreich.

Die Überschrift steht in <strong>-Tags:

Diese wiederum sind umschlossen von <p>-Tags, diese sind von einem <div>-Tag mit den Klassen elementor-text-editor elementor-clearfix umschlossen, was wiederum mit einem <div>-Tag mit der Klasse elementor-widget-container umschlossen wird.

In response.css(‘tag.class’) schreiben wir jetzt das Tag mit der Klasse bei der wir beginnen, in unserem Fall:

<em>response.css('div.</em>elementor-widget-container<em>')</em>

Jetzt gehen wir in Yield rein: Dort wo im Code Name steht kann jeder x-beliebige andere Begriff stehen. Entscheidend ist, was in a.css(‘tag.class tag::text’) steht.

Jetzt gehen wir die umschließenden Tags im HTML-Code ab, die nach dem div-Tag mit der elementor-widget-container-Klasse kommen:

Das wären: div.elementor-text-editor (die zweite Klasse ignorieren wir), dann kommt p dann strong und dann kommt schon der Text, was mit ::text gekennzeichnet wird.

Im Code schreiben wir deshalb jetzt folgendes:

    def parse(self, response):
        for a in response.css('div.elementor-widget-container'):
            yield {
                'Name': a.css('div.elementor-text-editor p strong::text').get(),
                
            }

Führen wir wieder mit scrapy crawl teacher den Crawler aus und fügen allerdings noch -O teacher.json an, was dafür sorgt, dass wir unser Ergebnis in einer Json-Datei abspeichern. Sehen wir es uns an:

Ergebnis

Wir haben offensichtlich die Überschrift rausgeparst. Das ü wird einfach nicht erkannt, und der Grund, dass in der Json mehrere weitere leere Name Items stehen, liegt daran, dass in der HTML noch Div-Tags mit der elementor-widget-container-Klasse gab.

Wie oben schon geschrieben gehen wir jetzt folgende Informationen der Dozenten an:

  • Namen
  • Rating
  • Stundensatz
  • Spezialisierungen

Fangen wir damit an uns den HTML-Code anzuschauen, der alles umschließt:

Das Tag, was alles umschließt ist das Div-Tag mit der Klasse teacher-card. Das packen wir response.css():

response.css('div.teacher-card')

Für den Dozenten-Namen zeige ich eine Technik, die ich gern zum parsen nutze:

Ergebnis:

'Name': a.css(div.teacher-card-left div.teacher-card-detail-top div.teacher-card-information h1 span::text)

Das muss ich jetzt nur noch in den kompletten Code einsetzen (und speichern nicht vergessen):

Für das Rating und den Stundesatz gehen wir genau so vor. Einen kleinen Unterschied gibt es bei den Spezialisierungen. Schauen wir uns hierzu zuerst das Frontend an:

Es gibt einen Namen, einen Stundensatz und ein Rating pro Dozent aber unterschiedlich viele Spezialisierungen. Das sieht man auch im HTML-Code-Ausschnitt:

Wir müssen hier mehrere Elemente gleichzeit parsen, das machen wir indem wir statt der .get()-Methode die .getall()-Methode nehmen.

Schauen wir uns das im finalen Code an:

Hier die Ausführung:

Automatisiertes Crawlen von mehreren Links

In der Regel will man mit einem Crawler nicht nur bestimmte URL`s einer Website crawlen sondern zum Beispiel alle URL`s aus der Sitemap oder alle aus dem Menü etc.

Für unsere Übungswebsite könnten wir diese Theoretisch händisch unter start_urls eintragen. Umso größer die Website umso mühseliger wird der Prozess, außerdem ist es ja eben genau das Ziel eines Crawlers uns diese eintönige Arbeit abzunehmen und sie zu automatisieren.

Hierfü müssen wir in die Parse Funktion noch folgendes einfügen:

yield from response.follow_all(css='<Strutkur, wie beim Parsen>', callback=self.parse)

Wenn wir wi in unserem Fall das Menü durchcrawlen wollen, müssen wir uns wieder den HTML-Code anschauen:

Uns interessiert bei dem li-Tag auch wieder nur die erste Klasse. Darauf aufbauen müssen wir unseren Code folgendermaßen anpassen:

yield from response.follow_all(css='li.menu-item a', callback=self.parse)

Bei der kompletten Parse-Funktion müssen wir dann nur, wie immer in Pyhton, schauen, dass wir die Tabs korrekt setzen:

Finaler Code und Ausführung

Für die, die das komplette Tutorial übersprungen haben, und direkt hier anfangen ist nur folgendes vor der Ausführung wichtig:

Ihr müsst euch bitte die ersten beiden Kapitel Virtual Environment/PIP Scrapy und Einrichtung eines neuen Projektes anschauen, weil ihr sonst mit dem Code nichts anfangen könnte.

Nachdem das Projekt erstellt und die virtuelle Umgebung gestartet wurde, einfach in den Ordner mila/spiders eine Python-Datei teacher_spider.py anlegen.

Dort kommt dann folgender finaler Code rein:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "teacher"
    start_urls = [
        'https://educado.io/seo/',
    ]

    def parse(self, response):
        for a in response.css('div.teacher-card'):
            yield {
                'Name': a.css('div.teacher-card-left div.teacher-card-detail-top div.teacher-card-information h1 span::text').get(),
                'Rating': a.css('div.teacher-card-left div.teacher-card-detail-top div.teacher-card-information span.teacher-rating span.rating-number::text').get(),
                'Stundensatz': a.css('div.teacher-card-left div.teacher-card-detail-bottom div.teacher-card-information div.teacher-card-rate div.teacher-card-hourly h2.teacher-price-rate span::text').get(),
                'Spezialisierung': a.css('div.teacher-card-left div.teacher-card-detail-top div.teacher-card-information div.teacher-language h2.teacher-card-tec-language div span.language span::text').getall(),
                
            }
            yield from response.follow_all(css='li.menu-item a', callback=self.parse)

Ausführung dann über die Console mit:

scrapy crawl teacher -O teacher.json

Hier nochmal im Video: