Arbeit mit scrapy#

Während sich requests und parsel für kleinere, lokale Arbeiten ohne weitere Automatisierung anbieten, stellt Scrapy umfangreiche Funktionalitäten für das hochautomatisierte und parallelisierte Abfragen und Extrahieren von Informationen aus Websites bereit - Scraping und Crawling.

Bevor wir uns anschauen, wie wir mit Scrapy die Informationen von https://quotes.toscrape.com/ extrahieren können, gilt es einige Begriffe und Konzepte zu erläutern:

  • Spiders sind Python-Klassen, welche den Scraping-Prozess für eine bestimmte Website regeln, inkl. wie weiteren Links gefolgt werden soll (crawling)

  • Selectors dienen der Extraktion von Informationen aus strukturierten Dateiformaten, wie z.B. HTML. Scrapy nutzt parsel und dessen Selectors.

  • Items sind Python-Objekte, die Spiders als Scraping-Ergebnis zurückgeben. Sie dienen als Container für die extrahierten Daten, während Item Loader diese Container mit den Daten füllen.

  • Item Pipelines definieren Verarbeitungsprozesse der extrahierten Daten, z.b. Bereinigung, Formatierung, Speichern in einer Datenbank, etc

  • Middleware erweitert die Funktionalitäten von Spiders, z.b. wie mit Fehlern umgegangen wird, das Definieren von UserAgents, etc

Scrapy wird primär über die Kommandozeile / Terminal genutzt. Der folgende Mix aus Kommandozeile und Jupyter-Notebook dient einer besseren Veranschaulichung des Ganzen.

ACHTUNG Für die produktive Nutzung von Scrapy empfiehlt sich ein umfassender Blick in die Dokumentation und das Tutorial, welches hier nur in Teilen betarchtet wird.

Projekt anlegen#

Scrapy arbeitet mit Projekten - je Projekt wird eine Ordnerstruktur mit Dateien für die Konfiguration und Python-Code angelegt.
Ein solches Projekt wird mit dem Befehl scrapy startproject project-name erzeugt.

Wir haben mit scrapy startproject scrape_quotes bereits ein solches Projekt angelegt, die entsprechende Ordnerstruktur findest du in scrape_quotes. Dieses enthält bereits Templates und vorbereitete Dateien für spiders, items, pipelines, middleware und weitere Einstellungen.

Hier ein Überblick zu den erstellten Dateien:

|-scrape_quotes
|    |-scrapy.cfg (allgemeine Konfiguration des Projekts)
|    |-scrape_quotes (Unterordner des Projekts)
|        |-items.py (Python-Datei zur Defintion von Items)
|        |-middleware.py (Python-Datei zur Defintion von Middleware)
|        |-pipelines.py (Python-Datei zur Defintion von Item Pipelines)
|        |-settings.py (Python-Datei für Einstellungen)
|        |-spiders (Unterordner für Spiders)
|                |-authors_spider.py (Python-Datei für Scraping von AutorInnen)
|                |-headers_spider.py (Python-Datei zur Demonastration der Header-Inhalte)
|                |-quotes_spider.py (Python-Datei für Scraping von Zitaten)

Spider definieren#

Im Unterordner spiders können einzelne Spiders für das Crawling und Scraping definiert werden. Hier sind bereits 3 Spider vorbereitet worden. Am Beispiel von authors_spider.py schauen wir uns den Aufbau eines Spiders mal an:

# Inhalt von authors_spider.py
# Import von scrapy für die Nutzung innerhalb des Spiders
import scrapy

# Defintion einer Klasse basierend auf dem Typ "scrapy.Spider"
# Siehe: https://docs.scrapy.org/en/latest/topics/spiders.html#scrapy-spider
class AuthorSpider(scrapy.Spider):
    # Einzigartige Benennung des Spider, um damit aufgerufen werden zu können
    name = "authors"
    # Einschränkung der Domain, die ein Spider scrapen/crawlen darf
    allowed_domains = ["quotes.toscrape.com"]
    # Startseite für das scrapen/crawlen
    start_urls = ["https://quotes.toscrape.com/"]

    # Definition einer Methode zum parsen einer HTTP Response
    # self: diese Spider Klasse
    # response: die HTML vom GET Request an die Website
    def parse(self, response):
        # Extraktion aller Elemente mit Klasse "author" und deren zugehöriger Link
        # Genutzt wird ein CSS-Selector
        author_page_links = response.css(".author + a")
        # Aufrufen aller extrahierten Links
        # Parsen der Inhalte mit u.a. Methode "parse_author"
        # Zurückgeben der Inhalte via yield
        yield from response.follow_all(author_page_links, self.parse_author)
        # Extraktion aller Elemente, um zur nächsten Seite zu blättern und deren zugehöriger Link
        # Genutzt wird ein CSS-Selector
        pagination_links = response.css("li.next a")
        # Aufruf der nächsten Seite und Übergabe an diese Methode "parse"
        yield from response.follow_all(pagination_links, self.parse)

    # Definition einer Methode zum parsen von Author-Elementen
    def parse_author(self, response):
        # Definition einer Methode um Textinhalte aus einem HTML-Element zu extrahieren
        def extract_with_css(query):
            return response.css(query).get(default="").strip()
        # Extraktion aller Infos zu einer AutorIn, rückgabe als Python-Dictionary        
        yield {
            "name": extract_with_css("h3.author-title::text"),
            "birthdate": extract_with_css(".author-born-date::text"),
            "bio": extract_with_css(".author-description::text"),
        }

Crawling ausführen#

Im Projektordner können wir jetzt das Crawling und die Extraktion für einen spezifischen Spider starten, wir nutzen hierfür den Befehl scrapy crawl spider-name.

# Starten des Spiders mit dem Namen "authors"
!cd scrape_quotes && scrapy crawl authors
# Starten des Spiders mit dem Namen "quotes"
!cd scrape_quotes && scrapy crawl quotes

Speichern der extrahierten Daten#

Scrapy unterstützt verschiedene Methoden für das Speichern der extrahierten Daten. Eine davon sind sogenannte Feed Exports, womit die extrahierten Daten z.B. direkt in eine JSON geschrieben werden können.

Die folgenden Befehle starten das Crawling und das Scraping der definierten Spider, die Ergebnisse werden dann zeilenweise als JSON-Objekte in die jeweilige Datei geschrieben. Durch den Parameter -o werden neue Daten an die bestehende Datei angehängt, der Parameter -O überschreibt bestehende Dateien.

# Starten des Spiders mit dem Namen "authors", Speichern der extrahierten Daten in JSON-Datei "authors.jsonl"
!cd scrape_quotes && scrapy crawl authors -O authors.jsonl
# Starten des Spiders mit dem Namen "quotes", Speichern der extrahierten Daten in JSON-Datei "quotes.jsonl"
!cd scrape_quotes && scrapy crawl quotes -O quotes.jsonl

Pipelines für Verarbeitung#

Mit Pipelines lassen sich die durch die Spider erstellten Items weiter verarbeiten, um die Daten z.B. zu bereinigen oder zu validieren, um Duplikate zu entfernen, oder die Daten in einer Datenbank zu speichern. Pipelines müssen in der Datei pipelines.pydefiniert werden, um sie aktivieren und ausführen zu können. Schauen wir uns das am Beispiel an:

# Inhalt von pipelines.py
# Datei zur Defintion von Item Pipelines

# Import einer Fehlermeldung für das Aussortieren von Items
from scrapy.exceptions import DropItem

# Defintion einer Klasse für eine spezifische Pipeline
class ScrapeQuotesPipeline:
    # Defintion einer Methode zur verarbeitung eines Items
    def process_item(self, item, spider):
        # Prüfung, ob das Item von Spider "quotes" erzeugt wurde
        if spider.name == 'quotes':
            # Prüfung, ob das Item das Feld "text" enthält
            if item['text']:
                # Tokenization des Inhaltes von "text" und Speichern in neuem Feld "quote_tokens"
                item['quote_tokens'] = item['text'].split()
                # Rückgabe des Items aus der Pipeline
                return item
            else:
                # Fehlermeldung und Verwerfen des Items, falls es kein Feld "text" gibt
                raise DropItem("Missing text for tokenization")
        else:
            return item

Um diese Pipeline automatisch auszuführen, muss sie in der Datei settings.py aktiviert werden:

...
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    "scrape_quotes.pipelines.ScrapeQuotesPipeline": 300,
}
...

Um die Pipeline zu deaktiveren, kann sie einfach mit einem Kommentar versehen werden:

...
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
#    "scrape_quotes.pipelines.ScrapeQuotesPipeline": 300,
}
...

Middleware für anderen UserAgent#

Bei einer Anfrage an einen Server wird eine Information zum sogenannten UserAgent mitgesendet. Der entsprechende UserAgent Header ist eine charakteristische Zeichenfolge, mit der Server oder andere Netzwerk-Teilnehmer die Anwendung, das Betriebssystem, den Hersteller und/oder die Version der anfragenden Anwendung identifizieren können. Server können die Nutzung von Websites für bestimmte UserAgents einschränken, z.b. für Bots oder Crawler.

  • Welchen UserAgent nutze ich: https://useragentstring.com/

  • Scrapy hat standardmäßig den UserAgent: “Scrapy/Versionsnummer (+https://scrapy.org)”

Über eine Middleware kann Scrapy so angepasst werden, dass es “normale” Netzwerk-Teilnehmer simuliert.

Führen wir das Scraping zunächst einmal ohne eine solche Middleware aus und sehen uns an, was headers_spider.py zurück gibt.

# Starten des Spiders mit dem Namen "headers"
!cd scrape_quotes && scrapy crawl headers
# In der Augabe wird der UserAgent "Scrapy/Versionsnummer (+https://scrapy.org)" angezeigt

In der Datei settings.py deaktivieren wir zunächst den Standardwert USER_AGENT.
Wir defineren zudem eine Liste von realistischen UserAgents in USER_AGENTS.

...
# Crawl responsibly by identifying yourself (and your website) on the user-agent
# USER_AGENT = "scrape_quotes (+http://www.yourdomain.com)"
# Liste von realen UserAgents
USER_AGENTS = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15'
]
...

In der Datei middleware.py definieren wir nun eine Middleware für das zufällige Wechseln der realistischen UserAgents:

...
# Middelware für zufällige User Agent Rotation
class UserAgentRotatorMiddleware:
    def __init__(self, user_agents):
        self.user_agents = user_agents

    @classmethod
    def from_crawler(cls, crawler):
        # Die Liste der UserAgents aus settings.py einlesen
        user_agents = crawler.settings.get('USER_AGENTS', [])
        return cls(user_agents)

    def process_request(self, request, spider):
        # Bei jedem Request zufällig einen UserAgents der Liste wählen
        user_agent = random.choice(self.user_agents)
        # Diesen UserAgents im HTTP Request Header setzen
        request.headers['User-Agent'] = user_agent

...

Um diese Middleware automatisch auszuführen, muss sie in der Datei settings.py aktiviert werden:

...
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
#    "scrape_quotes.middlewares.ScrapeQuotesDownloaderMiddleware": 543,
    "scrape_quotes.middlewares.UserAgentRotatorMiddleware": 400,
}
...

Um die Middleware zu deaktiveren, kann sie einfach mit einem Kommentar versehen werden:

...
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
#    "scrape_quotes.middlewares.ScrapeQuotesDownloaderMiddleware": 543,
#    "scrape_quotes.middlewares.UserAgentRotatorMiddleware": 400,
}
...

Mit aktivierter Middleware für das Setzen des UserAgents wird uns nun ein entsprechend angepasster Eintrag im HTTP Header angezeigt:

# Starten des Spiders mit dem Namen "headers"
!cd scrape_quotes && scrapy crawl headers
# In der Augabe wird einer der zufällig gesetzten realistischen UserAgents angezeigt