Scrapy ist das Werkzeug, zu dem du aufsteigst, wenn deine requests-Skripte nicht mehr skalieren. Es ist ein vollwertiges Crawling-Framework für Python: Du schreibst eine kleine Spider-Klasse, die festlegt, was extrahiert und welchen Links gefolgt werden soll, und Scrapy übernimmt Scheduling, Nebenläufigkeit, Retries, Deduplizierung und Export. Dieses Tutorial führt dich von scrapy startproject bis zu einem zweistufigen Crawl mit produktionssicheren Einstellungen — durchgängig in aktueller Scrapy-2.x-Syntax.
Warum Scrapy statt requests + BeautifulSoup?
Wenn du schon ein paar Scraper mit requests und BeautifulSoup geschrieben hast — der Stack, den unser Python web scraping guide und das BeautifulSoup tutorial behandeln —, kennst du das Muster bereits: Seite holen, parsen, wiederholen. Das funktioniert glänzend für eine Seite oder eine Liste. Es fällt auseinander, sobald die Aufgabe zum Crawling wird: viele Seiten besuchen, unterwegs Links entdecken und dieselbe URL nicht zweimal abrufen. Diese Entdeckungs- und Scheduling-Schleife ist what web crawling actually is, und genau dieser Teil gehört Scrapy:
- Ein Scheduler und eine Request-Queue. Du yieldest Requests; Scrapy entscheidet, wann sie gesendet werden, wie viele gleichzeitig laufen, und wiederholt die Fehlschläge.
- Eine URL-Frontier mit Deduplizierung. Folge demselben Link von fünf Seiten aus, und Scrapy ruft ihn nur einmal ab.
- Item Pipelines. Jeder gescrapte Datensatz durchläuft eine Kette kleiner Klassen, die ihn bereinigen, validieren oder speichern — getrennt von der Parsing-Logik.
- Feed Exports. Ausgabe als JSON, JSON Lines und CSV über ein Command-Line-Flag, ohne Serialisierungscode.
Um den Kompromiss klar zu benennen: BeautifulSoup ist eine Parsing-Bibliothek und eine großartige dazu; Scrapy bringt eine eigene Selector-Engine mit, ist aber eine schwerere Abhängigkeit mit steilerer Lernkurve. Für eine einzelne Seite ist Scrapy überdimensioniert. Für fünfhundert Seiten hinter Pagination ersetzt es hundert Zeilen Queue-und-Retry-Klempnerei, die du sonst einmal schlecht schreiben und ewig debuggen würdest.
Projekt-Setup: Was startproject tatsächlich erstellt
Installiere Scrapy (ein virtuelles Environment wird dringend empfohlen) und generiere ein Projekt:
pip install scrapy
scrapy startproject quotes_crawler
cd quotes_crawler
Der generierte Baum sieht so aus, und jede Datei hat eine Aufgabe:
quotes_crawler/
├── scrapy.cfg # deploy/config entry point — tells Scrapy where settings live
└── quotes_crawler/
├── items.py # optional typed containers for scraped data
├── middlewares.py # hooks into the request/response cycle (headers, proxies)
├── pipelines.py # post-processing for scraped items (clean, validate, store)
├── settings.py # project-wide knobs: concurrency, delays, throttling
└── spiders/ # your spider classes live here — one class per crawl target
items.py und middlewares.py kannst du vorerst ignorieren — einfache dicts und die Standard-Middleware reichen für ein erstes Projekt. Die beiden Dateien, die du heute tatsächlich anfasst, sind eine neue Spider in spiders/ und, später, settings.py und pipelines.py.
Erzeuge das Grundgerüst einer Spider mit genspider:
scrapy genspider quotes quotes.toscrape.com
Deine erste Spider
Wir scrapen quotes.toscrape.com, die Sandbox-Website, die das offizielle Scrapy-Tutorial verwendet. Ersetze die generierte spiders/quotes.py durch:
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = ["https://quotes.toscrape.com/page/1/"]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
Drei Ideen tragen das gesamte Framework:
start_urlsist der Startpunkt des Crawls. Scrapy ruft jede URL ab und übergibt die Response anparse(). (Seit Scrapy 2.13 kannst du stattdessenasync def start(self)für dynamische Start-Requests definieren;start_urlsbleibt der idiomatische einfache Fall, aber beachte, dass die ältere Methodestart_requests()deprecated ist.)response.css()wählt Elemente mit CSS-Selektoren aus —::textextrahiert Textknoten,::attr(href)extrahiert ein Attribut,.get()liefert den ersten Treffer oder None,.getall()liefert jeden Treffer als Liste.response.xpath()steht bereit, wenn CSS nicht ausdrücken kann, was du brauchst; unter der Haube werden CSS-Selektoren ohnehin in XPath übersetzt. Das ist dasselbe HTML parsing-Problem, das BeautifulSoup löst, nur mit Scrapys Selector-API.yieldein dict, und Scrapy behandelt es als gescraptes Item — keine Rückgabewerte, keine sich anhäufenden Listen.
Führe sie aus und exportiere direkt nach JSON:
scrapy crawl quotes -O quotes.json
Das -O-Flag überschreibt die Ausgabedatei bei jedem Lauf; das kleingeschriebene -o hängt stattdessen an, was vor allem beim JSON-Lines-Format nützlich ist (-o quotes.jsonl). Öffne quotes.json, und du findest zehn strukturierte Datensätze — eine Seite voll, weil noch nichts Links folgt.
Links folgen: Pagination und zweistufige Crawls
Crawling beginnt, wenn parse() neben Items auch Requests yieldet. Die Quotes-Website hat einen Next-Button, und response.follow() erledigt ihn in zwei Zeilen — es akzeptiert relative URLs direkt, also kein manuelles urljoin:
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("div.tags a.tag::text").getall(),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
Führe scrapy crawl quotes -O quotes.json erneut aus, und du bekommst alle zehn Seiten — ungefähr hundert Zitate. Der Scheduler hat jeden Next-Link eingereiht, abgerufen und die Response zurück durch parse() geleitet. Doppelte URLs wären automatisch herausgefiltert worden.
Das Muster, das echte Projekte erschließt, ist der zweistufige Crawl: Eine Listenseite yieldet Links zu Detailseiten, und ein zweiter Callback parst jede Detailseite. Hier ist eine Spider, die die Zitat-Listen durchläuft, aber Daten von der Biografieseite jedes Autors extrahiert:
import scrapy
class AuthorSpider(scrapy.Spider):
name = "authors"
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
# Level 1: listing pages — follow every author link
author_links = response.css(".author + a")
yield from response.follow_all(author_links, callback=self.parse_author)
# ...and keep paginating the listing itself
yield from response.follow_all(css="li.next a", callback=self.parse)
def parse_author(self, response):
# Level 2: detail pages
yield {
"name": response.css("h3.author-title::text").get().strip(),
"born": response.css(".author-born-date::text").get(),
"bio": response.css(".author-description::text").get().strip()[:200],
}
response.follow_all() yieldet einen Request pro passendem Link, und jeder Callback kümmert sich nur um seinen eigenen Seitentyp. Auch wenn viele Zitate auf denselben Autor verweisen, wird jede Biografie nur einmal abgerufen — wieder Deduplizierung. Dieselbe Form skaliert auf einen E-Commerce-Katalog (Kategorieseiten zu Produktseiten, wie auf books.toscrape.com) oder ein Jobportal (Suchergebnisse zu Stellenanzeigen).
Für Crawls, bei denen „folge jedem Link, der diesem Muster entspricht" die gesamte Strategie ist, bringt Scrapy CrawlSpider mit, eine Subklasse, in der du Rule-Objekte mit einem LinkExtractor deklarierst, statt die Follow-Logik von Hand zu schreiben. Beginne mit einer schlichten Spider — du verstehst genau, was sie abruft — und greife zu CrawlSpider, wenn deine Follow-Regeln repetitiv werden.
Produktionseinstellungen: schnell, aber nicht rücksichtslos
Die Standardwerte sind auf die Sandbox abgestimmt, nicht auf den Server eines anderen. Scrapy führt bereitwillig 16 gleichzeitige Requests ohne jede Verzögerung aus — ein guter Weg, um in rate limiting zu laufen oder einer kleinen Website zu schaden. Öffne vor jedem echten Crawl settings.py:
# settings.py — a polite production baseline
# Respect robots.txt (on by default in new projects — leave it on)
ROBOTSTXT_OBEY = True
# Identify yourself honestly
USER_AGENT = "quotes_crawler (+https://yourdomain.example/contact)"
# Cap concurrency per site and add a base delay
CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 1.0 # seconds; Scrapy randomizes it a bit by default
# Let Scrapy adapt speed to the server's actual latency
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5.0
AUTOTHROTTLE_MAX_DELAY = 60.0
AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0
AutoThrottle ist die Einstellung, die die meisten Tutorials überspringen, und die, die am meisten zu kennen lohnt. Sie misst die Response-Latenz und passt die Verzögerung dynamisch an — wenn der Server langsamer wird oder Fehler liefert, geht Scrapy Richtung AUTOTHROTTLE_MAX_DELAY zurück; wenn er gesund ist, beschleunigt Scrapy Richtung deiner Ziel-Nebenläufigkeit. DOWNLOAD_DELAY wirkt als Untergrenze, die nie unterschritten wird, und CONCURRENT_REQUESTS_PER_DOMAIN bleibt eine harte Obergrenze. Setze AUTOTHROTTLE_DEBUG = True vorübergehend, um die Throttle-Entscheidungen im Log zu beobachten.
ROBOTSTXT_OBEY verdient einen Satz: Es sorgt dafür, dass Scrapy vor dem Crawlen die robots.txt-Regeln jeder Website abruft und befolgt. In generierten Projekten ist es standardmäßig aktiviert. Lass es aktiviert.
Eine minimale Item Pipeline
Pipelines halten die Bereinigung aus deinem Parse-Code heraus. Jedes gescrapte Item durchläuft process_item() in jeder aktivierten Pipeline-Klasse:
# pipelines.py
from scrapy.exceptions import DropItem
class CleanQuotePipeline:
def process_item(self, item, spider):
if not item.get("text"):
raise DropItem("missing quote text")
item["text"] = item["text"].strip("“” ") # strip curly quotes
item["author"] = item["author"].strip()
return item
Aktiviere sie in settings.py — die Zahl ist eine Reihenfolge-Priorität, niedriger läuft zuerst:
ITEM_PIPELINES = {
"quotes_crawler.pipelines.CleanQuotePipeline": 300,
}
Dieselbe Form übernimmt Validierung, Deduplizierung nach Feld oder das Schreiben in eine Datenbank (Pipelines bekommen zudem open_spider- und close_spider-Hooks für Verbindungen). Beim Parsen geht es weiter um Selektoren; die Datenqualität gehört den Pipelines.
- Spider yieldet dicts aus parse() und folgt Links mit response.follow / follow_all
- Selektoren wurden in scrapy shell getestet, bevor sie in die Spider kamen
- ROBOTSTXT_OBEY ist aktiviert und der USER_AGENT identifiziert deinen Crawler
- AUTOTHROTTLE_ENABLED ist true, mit DOWNLOAD_DELAY als Untergrenze
- Bereinigung und Validierung leben in einer Pipeline, nicht in parse()
- Ausgabe verifiziert mit scrapy crawl name -O output.json
Wo Scrapy an Grenzen stößt: JavaScript und Anti-Bot-Systeme
Scrapy ist hervorragend in dem, was es tut — beim Crawlen von statischem HTML im großen Maßstab schwer zu schlagen. Aber zwei Wände solltest du kennen, bevor du gegen sie läufst.
JavaScript-gerenderte Seiten. Scrapys Downloader holt rohes HTML; er führt keine Browser-Engine aus. Erscheinen die Daten erst nach clientseitigem Rendering, treffen deine Selektoren nichts. Die Standardlösung ist das scrapy-playwright-Plugin, das Requests durch einen Headless-Browser leitet und dabei Scrapys Scheduling beibehält — oder ein eigenständiger Browser-Stack, wie in unserem Playwright scraping guide beschrieben. So oder so kostet Rendering echte CPU und echten Arbeitsspeicher pro Seite.
Anti-Bot-Systeme im großen Maßstab. Große Plattformen erstellen Fingerprints von TLS-Handshakes, bewerten die IP-Reputation und stellen JavaScript-Challenges. Eine gut gedrosselte Scrapy-Spider von einer einzigen IP wird dort trotzdem blockiert, und das Stapeln von Proxy-Middleware und Header-Tricks wird zu einem zweiten Engineering-Projekt — dieses Wettrüsten behandeln wir in scraping sites that block bots.
Für solche Ziele ist der pragmatische Zug, Scrapy für die Websites zu behalten, die es bewältigt, und für die anderen eine Scraping API aufzurufen. Crawlora stellt abgehärtete Plattformen als dokumentierte Endpoints bereit, die strukturiertes JSON zurückgeben — hier eine Amazon-Suche, ganz ohne Proxys oder Parser auf deiner Seite:
import requests
resp = requests.get(
"https://api.crawlora.net/api/v1/amazon/search",
params={"k": "mechanical keyboard"},
headers={"x-api-key": "YOUR_API_KEY"},
)
results = resp.json()["data"] # structured listings, not HTML
Du kannst sie sogar aus einer Pipeline oder Spider heraus aufrufen, um gecrawlte Datensätze anzureichern. Die Abrechnung erfolgt pay-on-success — dir werden nur erfolgreiche 2xx-Responses berechnet — und die kostenlose Stufe umfasst 2.000 Credits/Monat, ohne Karte. Durchstöbere den Endpoint-Katalog in den docs oder wirf für die Mengenrechnung einen Blick auf pricing.
Fazit
Du hast jetzt die komplette Scrapy-Schleife: Grundgerüst mit startproject und genspider, Extraktion mit response.css, dicts yielden, Links mit response.follow folgen, Export mit -O, Drosselung mit AutoThrottle und Bereinigung mit Pipelines. Das deckt einen sehr großen Anteil echter Crawling-Arbeit ab. Wenn ein Ziel in JavaScript rendert oder sich mit Anti-Bot-Systemen wehrt, schnall scrapy-playwright dran oder übergib genau diese eine Website an eine API — und lass deine Spider weiter das tun, worin sie wirklich großartig ist.
Stößt deine Spider auf Websites, die sie nicht crawlen kann?
Behalte Scrapy für die einfachen Ziele und rufe für die schwierigen dokumentierte Endpoints auf — strukturiertes JSON, verwaltete Proxys und Retries. 2.000 kostenlose Credits pro Monat, ohne Karte.
Weiterführende Lektüre: der Python web scraping guide für das große Ganze, das BeautifulSoup tutorial, wenn du den leichteren Stack willst, und web scraping with Playwright für JavaScript-lastige Ziele.
Häufig gestellte Fragen
Wofür wird Scrapy verwendet?
Scrapy ist ein Python-Framework zum Crawlen von Websites und zum Extrahieren strukturierter Daten. Anders als eine Parsing-Bibliothek verwaltet es den gesamten Crawl: Requests planen, Links folgen, URLs deduplizieren, Fehlschläge wiederholen, das Tempo drosseln und Ergebnisse nach JSON oder CSV exportieren. Es ist das Standardwerkzeug, sobald ein Projekt über das Abrufen einer Handvoll Seiten hinauswächst.
Ist Scrapy besser als BeautifulSoup?
Sie lösen unterschiedliche Probleme. BeautifulSoup parst ein einzelnes HTML-Dokument; Scrapy ist ein vollwertiges Crawling-Framework mit eigenen Selektoren plus Scheduling, Nebenläufigkeit, Deduplizierung, Pipelines und Feed Exports. Für eine einzelne Seite ist requests plus BeautifulSoup einfacher. Fürs Crawlen vieler Seiten hinter Pagination oder per Linkverfolgung ersetzt Scrapy die Queue-und-Retry-Klempnerei, die du sonst selbst schreiben müsstest.
Kann Scrapy mit JavaScript umgehen?
Nicht von allein. Scrapy holt rohes HTML und führt keine Browser-Engine aus, sodass clientseitig gerenderte Inhalte für seine Selektoren unsichtbar sind. Die übliche Lösung ist das scrapy-playwright-Plugin, das Seiten in Headless-Chromium rendert und dabei Scrapys Scheduling beibehält, oder JavaScript-lastige Websites an eine Scraping API zu übergeben, die das Rendering für dich übernimmt.
Wie führe ich einen Scrapy-Spider aus und speichere die Ausgabe?
Führe aus dem Projektverzeichnis scrapy crawl spidername -O output.json aus. Das -O-Flag überschreibt die Datei bei jedem Lauf; das kleingeschriebene -o hängt an, was am besten zu JSON Lines passt (output.jsonl). Scrapys Feed Exports unterstützen auch CSV und XML ohne zusätzlichen Code.
Wie folgt Scrapy Links und paginiert?
Dein parse()-Callback yieldet Requests neben Items. response.follow() akzeptiert eine relative URL direkt aus einem Selektor, und response.follow_all() yieldet einen Request pro passendem Link — etwa um jedem Next-Link oder jedem Detailseiten-Link zu einem zweiten Callback zu folgen. Doppelte URLs filtert Scrapy automatisch.
Wie verhindere ich, dass Scrapy eine Website überlastet?
Aktiviere AutoThrottle (AUTOTHROTTLE_ENABLED = True), damit Scrapy seine Verzögerung an die Latenz des Servers anpasst, setze DOWNLOAD_DELAY als Untergrenze, begrenze CONCURRENT_REQUESTS_PER_DOMAIN und lass ROBOTSTXT_OBEY aktiviert. Diese Einstellungen liegen in der settings.py des Projekts.
Was sind Scrapy Item Pipelines?
Pipelines sind kleine Klassen, die jedes gescrapte Item nach dem Parsen durchläuft. Jede definiert process_item(), um das Item zu bereinigen, zu validieren, zu deduplizieren oder zu speichern — und wirft DropItem, um schlechte Datensätze zu verwerfen. Sie werden in ITEM_PIPELINES in der settings.py mit einer Prioritätszahl aktiviert und halten so die Datenqualitäts-Logik aus dem Parse-Code deines Spiders heraus.
