Screaming Frog ist der Standard-Crawler für die meisten SEOs, aber Sie sind wahrscheinlich schon an seine Grenzen gestoßen: das 500-URL-Limit in der kostenlosen Version, RAM-Überlastung bei großen Seiten oder der Wunsch, Crawls zu automatisieren, ohne eine GUI zu beaufsichtigen. Scrapy ist das Open-Source Python-Framework, das diese Limits beseitigt.

Wenn Sie npm install oder git clone ausführen können, können Sie auch Scrapy ausführen. Die Lernkurve ist real, aber machbar, besonders wenn Sie sich bereits durch agentische Coding-Workflows mit CLI-Tools vertraut machen.

Warum Scrapy?

Hauptvorteile

Screaming Frog funktioniert gut für schnelle Audits. Aber es hat Grenzen:

Einschränkung Auswirkung
500 URL Gratis-Limit Erfordert 259$/Jahr Lizenz für größere Seiten
Speicherhungrig Große Crawls können 8GB+ RAM verbrauchen
GUI-abhängig Schwierig zu automatisieren oder zu planen
Begrenzte Anpassung Konfigurationsoptionen sind fest

Scrapy löst diese Probleme:

Scrapy Was Sie bekommen
Kostenlos und Open-Source Keine URL-Limits, keine Lizenzgebühren
Geringerer Speicherverbrauch Festplatten-gestützte Warteschlangen halten RAM im Rahmen
CLI-nativ Skriptfähig, cron-fähig, CI/CD-bereit
Volle Python-Anpassung Extrahieren Sie, was Sie brauchen, filtern Sie, wie Sie wollen
Pausieren/Fortsetzen Stoppen und setzen Sie große Crawls jederzeit fort
Scrapy wird Screaming Frog nicht für alles ersetzen. Schnelle Audits sind in einer GUI immer noch schneller. Aber für großangelegte Crawls, Automatisierung und benutzerdefinierte Extraktion lohnt es sich, es in Ihrem Toolkit zu haben.

Installation

Setup

Scrapy läuft auf Python. Verwenden Sie eine virtuelle Umgebung, um alles sauber zu halten:

Debian/Ubuntu:

sudo apt install python3.11-venv
python3 -m venv venv
source venv/bin/activate
pip install scrapy

macOS:

python3 -m venv venv
source venv/bin/activate
pip install scrapy

Windows:

python -m venv venv
venv\Scripts\activate
pip install scrapy
Virtuelle Umgebungen sind wichtig
Verwenden Sie immer ein venv. Globale Installation verursacht Abhängigkeitskonflikte und beeinträchtigt die Reproduzierbarkeit.

Ein Projekt erstellen

Mit installiertem Scrapy:

scrapy startproject myproject
cd myproject
scrapy genspider sitename example.com

Dies erstellt:

myproject/
    scrapy.cfg
    myproject/
        __init__.py
        items.py
        middlewares.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            sitename.py

Spider-Code gehört in spiders/sitename.py. Konfiguration liegt in settings.py.

Einstellungen für höfliches Crawlen

Kritisch

Konfigurieren Sie settings.py, bevor Sie irgendetwas ausführen. Blockiert zu werden verschwendet mehr Zeit als langsames Crawlen.

# Höfliches Crawlen
CONCURRENT_REQUESTS_PER_DOMAIN = 5
DOWNLOAD_DELAY = 1
ROBOTSTXT_OBEY = True

# AutoThrottle - passt Geschwindigkeit basierend auf Server-Antwort an
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = True

# Sicherheitsgrenzen
CLOSESPIDER_PAGECOUNT = 10000

# Ausgabe
FEED_EXPORT_ENCODING = "utf-8"
Mit aktiviertem AutoThrottle sind 5 gleichzeitige Anfragen ein vernünftiger Ausgangspunkt. AutoThrottle wird automatisch zurückfahren, wenn der Server Probleme hat. Ohne AutoThrottle beginnen Sie niedriger bei 1-3.

AutoThrottle

AutoThrottle überwacht Server-Antwortzeiten und passt die Crawl-Geschwindigkeit automatisch an:

  • Schnelle Antworten → beschleunigt
  • Langsame Antworten → verlangsamt
  • Fehler/Timeouts → verlangsamt deutlich

Im Gegensatz zu Screaming Frogs festen Verzögerungen passt es sich an tatsächliche Serverbedingungen an.

Status-Code-Behandlung

Standardmäßig verwirft Scrapys HttpErrorMiddleware nicht-2xx-Antworten stillschweigend. Das bedeutet, 404s, 301s, 500s werden verworfen, bevor sie Ihren Callback erreichen. Ihr Crawl zeigt möglicherweise 100% 200-Statuscodes, nicht weil die Seite perfekt ist, sondern weil Fehler herausgefiltert werden.

Fügen Sie dies zu Ihrer Spider-Klasse hinzu, um alle Statuscodes zu erfassen:

handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]

Screaming Frog erfasst standardmäßig alle Statuscodes. Diese Einstellung bringt Scrapy mit diesem Verhalten in Einklang.

Praxisleistung

Benchmarks

Tatsächliche Zahlen von einem Test-Crawl mit 5 gleichzeitigen Anfragen und aktiviertem AutoThrottle:

Crawl-Fortschritt Seiten/Minute Notizen
0-200 Seiten 14-22 Anlaufphase
200-500 Seiten 10-12 Stabilisierung
500-1.000 Seiten 7-10 AutoThrottle passt an
1.000+ Seiten 5-7 Stabiler Zustand
Geschwindigkeit vs. Zuverlässigkeit
Diese Geschwindigkeiten wirken langsam. Das ist der Sinn. AutoThrottle priorisiert Server-Gesundheit über rohe Geschwindigkeit. Blockiert zu werden und neu zu starten verschwendet mehr Zeit als ein methodischer Crawl.

Funktionsvergleich

Funktion Screaming Frog Scrapy
Kosten Kostenlos <500 URLs, ~259$/Jahr Kostenlos, Open Source
Max. Crawl-Größe Speicherbegrenzt Festplatten-gestützte Warteschlangen
Anpassung Begrenzte Konfigurationsoptionen Voller Python-Code
Planung Manuell oder Drittanbieter Native CLI, cron-fähig
Pausieren/Fortsetzen Ja Ja (mit JOBDIR)
Lernkurve Niedrig (GUI) Mittel (Code)
Rate-Limiting Grundlegende feste Verzögerungen AutoThrottle (adaptiv)
JavaScript-Rendering Optional (Chrome) Optional (playwright/splash)
Statuscodes Alle standardmäßig Erfordert Konfiguration
Subdomain-Filterung GUI-Checkboxen Code (flexibles Regex)
Export-Formate CSV, Excel, etc. JSON, CSV, XML, benutzerdefiniert
CI/CD-Integration Schwierig Nativ

URL-Filterung

Präzise Kontrolle

Screaming Frog verwendet Checkboxen. Scrapy verwendet Code. Der Kompromiss ist Lernkurve für Präzision.

Internationale Pfade ausschließen:

import re
from urllib.parse import urlparse

class MySiteSpider(scrapy.Spider):
    name = "mysite"
    allowed_domains = ["example.com", "www.example.com"]
    start_urls = ["https://www.example.com/"]

    # Überspringe internationale Pfade wie /uk/, /fr/, /de/
    EXCLUDED_PATTERNS = re.compile(
        r"/(in|au|th|es|hk|sg|ph|my|ca|cn|uk|kr|id|fr|vn|de|jp|nl|it|tw)/"
    )

    def filter_links(self, links):
        filtered = []
        for link in links:
            hostname = urlparse(link.url).hostname or ""
            if hostname not in ("example.com", "www.example.com"):
                continue
            if self.EXCLUDED_PATTERNS.search(link.url):
                continue
            filtered.append(link)
        return filtered

Sie können nach URL-Mustern, Query-Parametern, Response-Headern, Seiteninhalt oder jeder Kombination filtern.

Sitemap-Integration

URL-Entdeckung

Screaming Frog hat ein einfaches “Sitemap verwenden”-Kontrollkästchen. Scrapy erfordert benutzerdefinierten Code, gibt Ihnen aber volle Kontrolle darüber, wie Sitemaps geparst und in Ihren Crawl integriert werden.

Warum Sitemap-Unterstützung hinzufügen?

  • Entdeckt URLs, die nicht von der Hauptnavigation verlinkt sind
  • Findet verwaiste Seiten, die link-basiertes Crawlen verpassen würde
  • Holt die “offizielle” URL-Liste der Seite zum Vergleich
  • Kann mehr Seiten aufdecken als nur Links zu folgen
  • Essentiell für vollständige SEO-Audits

Fügen Sie diese Methoden zu Ihrem CrawlSpider hinzu, um Sitemap-Erkennung und -Parsing zu aktivieren:

def start_requests(self):
    # Zuerst robots.txt abrufen, um Sitemaps zu finden
    yield Request(
        "https://www.example.com/robots.txt",
        callback=self.parse_robots,
        errback=self.handle_error,
        dont_filter=True,
    )
    # Auch übliche Sitemap-Standorte direkt versuchen
    common_sitemaps = [
        "https://www.example.com/sitemap.xml",
        "https://www.example.com/sitemap_index.xml",
    ]
    for sitemap_url in common_sitemaps:
        yield Request(
            sitemap_url,
            callback=self.parse_sitemap,
            errback=self.handle_error,
            meta={"sitemap_url": sitemap_url},
        )
    # Auch normalen Crawl von der Startseite starten
    for url in self.start_urls:
        yield Request(url, callback=self.parse_start_url)

def parse_robots(self, response):
    """Parse robots.txt, um Sitemap-Deklarationen zu finden"""
    if response.status != 200:
        return
    for line in response.text.splitlines():
        line = line.strip()
        if line.lower().startswith("sitemap:"):
            sitemap_url = line.split(":", 1)[1].strip()
            if self.is_valid_url(sitemap_url):
                self.logger.info(f"Sitemap in robots.txt gefunden: {sitemap_url}")
                yield Request(
                    sitemap_url,
                    callback=self.parse_sitemap,
                    errback=self.handle_error,
                    meta={"sitemap_url": sitemap_url},
                )

def parse_sitemap(self, response):
    """Parse XML-Sitemap oder Sitemap-Index"""
    if response.status != 200:
        return
    content_type = response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore")
    # Prüfen, ob dies XML-Inhalt ist
    if "xml" not in content_type and not response.text.strip().startswith("<?xml"):
        return
    # Auf Sitemap-Index prüfen (enthält andere Sitemaps)
    sitemap_locs = response.xpath("//sitemap/loc/text()").getall()
    if sitemap_locs:
        self.logger.info(f"Sitemap-Index mit {len(sitemap_locs)} Sitemaps gefunden")
        for loc in sitemap_locs:
            if self.is_valid_url(loc):
                yield Request(
                    loc,
                    callback=self.parse_sitemap,
                    errback=self.handle_error,
                    meta={"sitemap_url": loc},
                )
    # URL-Einträge aus Sitemap parsen
    url_locs = response.xpath("//url/loc/text()").getall()
    if url_locs:
        self.logger.info(f"{len(url_locs)} URLs in Sitemap gefunden: {response.url}")
        for loc in url_locs:
            if self.is_valid_url(loc) and self.should_crawl_url(loc):
                yield Request(
                    loc,
                    callback=self.parse_page,
                    errback=self.handle_error,
                )

def parse_start_url(self, response):
    """Start-URL behandeln und Regeln auslösen"""
    yield from self.parse_page(response)
    yield from self._requests_to_follow(response)

def is_valid_url(self, url):
    """Prüfen, ob URL gültig ist und innerhalb erlaubter Domains liegt"""
    try:
        parsed = urlparse(url)
        hostname = parsed.hostname or ""
        return hostname in ("example.com", "www.example.com")
    except Exception:
        return False

def should_crawl_url(self, url):
    """Dieselbe Filterung wie filter_links anwenden"""
    if self.EXCLUDED_PATTERNS.search(url):
        return False
    return True

def handle_error(self, failure):
    """Request-Fehler elegant behandeln"""
    self.logger.warning(f"Request fehlgeschlagen: {failure.request.url}")

So funktioniert es:

  1. start_requests() überschreibt das Standardverhalten, um zuerst Sitemaps abzurufen
  2. parse_robots() findet Sitemap:-Zeilen in robots.txt
  3. parse_sitemap() behandelt sowohl Sitemap-Indizes als auch reguläre Sitemaps
  4. XPath //sitemap/loc findet verschachtelte Sitemaps in Sitemap-Index-Dateien
  5. XPath //url/loc findet tatsächliche Seiten-URLs
  6. Dieselbe Domain- und Musterfilterung gilt für Sitemap-URLs
  7. Scrapys eingebaute Deduplizierung verhindert doppeltes Crawlen von Seiten, die sowohl in Sitemaps als auch in Links gefunden werden
Funktion Screaming Frog Scrapy
Sitemap-Erkennung Kontrollkästchen Benutzerdefinierter Code
robots.txt-Parsing Automatisch Benutzerdefinierter Code
Sitemap-Index-Unterstützung Ja Ja (mit Code)
URL-Filterung GUI-Optionen Code (volle Kontrolle)
Mit Crawl zusammenführen Ja Ja
Benutzerdefinierte Sitemap-Standorte Manuelle Eingabe Code beliebiger Standort

Mit Sitemap-Integration können Sie verwaiste Seiten entdecken, die nicht von der Navigation verlinkt sind, alte archivierte Inhalte, die noch in Sitemaps aufgeführt sind, URL-Variationen mit oder ohne abschließenden Schrägstrich und Seiten, die von robots.txt blockiert werden, aber noch in der Sitemap vorhanden sind. Dies gibt ein vollständigeres Bild der Seite für SEO-Audits.

Pausieren und Fortsetzen

Essentiell

Für Crawls über 1.000 Seiten aktivieren Sie Pausieren/Fortsetzen mit JOBDIR:

scrapy crawl myspider -o output.json -s JOBDIR=crawl_state

Scrapy speichert den Status in crawl_state/. Drücken Sie Strg+C zum Pausieren. Führen Sie denselben Befehl aus, um fortzusetzen.

Verwenden Sie immer JOBDIR für Produktions-Crawls. Schützt vor Netzwerkproblemen, Systemneustarts oder wenn Sie einfach für den Tag aufhören müssen.

Der Status umfasst ausstehende URLs, gesehene URLs und die Anfragewarteschlange. Dies ist robuster als die Speichern/Laden-Funktion von Screaming Frog, da es dateibasiert ist und Systemneustarts übersteht.

JavaScript-Rendering

Scrapy ruft nur rohes HTML ab. Es rendert kein JavaScript. Das ist dasselbe, was curl zurückgibt.

Für die meisten SEO-Crawls ist das in Ordnung:

  • Meta-Tags, Canonicals und H1s sind normalerweise im initialen HTML
  • Suchmaschinen indexieren hauptsächlich server-gerenderten Inhalt
  • Die meisten E-Commerce- und Content-Seiten sind server-gerendert

Wenn Ihre Zielseite Inhalte client-seitig rendert, haben Sie Optionen:

Paket Hinweise
scrapy-playwright Verwendet Chromium/Firefox/WebKit. Empfohlen für moderne JS-Seiten
scrapy-splash Leichtgewichtig, Docker-basierter Renderer
scrapy-selenium Älterer Ansatz, funktioniert noch

JS-Rendering ist deutlich langsamer und ressourcenintensiver. Fügen Sie es nur hinzu, wenn die Seite es erfordert.

Screaming Frog hat einen ähnlichen Kompromiss. Das Aktivieren von JavaScript-Rendering verwendet Chrome im Hintergrund und verlangsamt Crawls erheblich.

Speicherverwaltung

Bei ~1.300 Seiten mit vollständiger Feldextraktion:

  • Speicher: ~265 MB
  • CPU: ~4%

Die Verwendung von JOBDIR verschiebt Anfragewarteschlangen auf die Festplatte und hält den Speicher niedrig. Für sehr große Crawls (100k+ URLs) fügen Sie diese Einstellungen hinzu:

MEMUSAGE_LIMIT_MB = 1024
MEMUSAGE_WARNING_MB = 800
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'

Dies begrenzt die Speichernutzung und erzwingt festplatten-gestützte Warteschlangen für den Scheduler.

Ausgabedaten

Anpassbar

Grundlegende Spider-Ausgabe:

{
    "url": "https://www.example.com/page/",
    "title": "Seitentitel hier",
    "status": 200
}

Für SEO-Crawls benötigen Sie Felder ähnlich dem, was Screaming Frog exportiert:

def parse_page(self, response):
    yield {
        "url": response.url,
        "status": response.status,
        "title": response.css("title::text").get(),
        "meta_description": response.css("meta[name='description']::attr(content)").get(),
        "meta_robots": response.css("meta[name='robots']::attr(content)").get(),
        "h1": response.css("h1::text").get(),
        "canonical": response.css("link[rel='canonical']::attr(href)").get(),
        "og_title": response.css("meta[property='og:title']::attr(content)").get(),
        "og_description": response.css("meta[property='og:description']::attr(content)").get(),
        "word_count": len(response.text.split()) if response.status == 200 else None,
        "content_type": response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore"),
    }

Fügen Sie Felder basierend auf Ihren Anforderungen hinzu oder entfernen Sie sie. CSS-Selektoren funktionieren für jedes On-Page-Element.

Export-Formate: JSON (-o output.json), JSON Lines (-o output.jsonl), CSV (-o output.csv), XML (-o output.xml).

JSON Lines ist am besten für große Crawls. Dateien sind während des Crawls zeilenweise gültig, sodass Sie mit tail -f überwachen können. Standard-JSON ist erst nach Abschluss des Crawls gültig.

Screaming Frog → Scrapy

Übersetzungsanleitung

SF-Workflows auf Scrapy abbilden:

Screaming Frog Aktion Scrapy Äquivalent
Neuen Crawl starten scrapy crawl spidername
Crawl-Verzögerung setzen DOWNLOAD_DELAY in Einstellungen
Gleichzeitige Threads begrenzen CONCURRENT_REQUESTS_PER_DOMAIN
robots.txt respektieren ROBOTSTXT_OBEY = True
Nach CSV exportieren -o output.csv
Crawl speichern/laden -s JOBDIR=crawl_state
Subdomains filtern Code im Spider (Regex)
Benutzerdefinierte Extraktion CSS/XPath-Selektoren in parse()

Denkweisenänderungen:

  1. Konfiguration ist Code. Bearbeiten Sie settings.py statt Checkboxen anzuklicken.
  2. Extraktion ist explizit. Sie schreiben, welche Daten erfasst werden sollen.
  3. Planung ist nativ. Fügen Sie Befehle zu cron oder CI/CD hinzu.
  4. Debugging sind Logs. Aktivieren Sie AUTOTHROTTLE_DEBUG, um zu sehen, was passiert.

Vollständiger Workflow

Mit den oben genannten Standardeinstellungen können Sie Scrapy in unter 15 Minuten installiert haben und crawlen:

python3 -m venv venv
source venv/bin/activate  # venv\Scripts\activate unter Windows
pip install scrapy
scrapy startproject urlcrawler
cd urlcrawler
scrapy genspider mysite example.com
# Bearbeiten Sie settings.py mit höflicher Crawl-Konfiguration
# Bearbeiten Sie spiders/mysite.py mit Ihrer Parse-Logik
scrapy crawl mysite -o urls.jsonl -s JOBDIR=crawl_state

Scrapy Shell

Wenn Sie benutzerdefinierte Konfigurationen erstellen, verwenden Sie Scrapy Shell, um Ihre Selektoren und Einstellungen interaktiv zu testen:

scrapy shell "https://example.com"

Dies öffnet eine interaktive Python-Konsole mit bereits geladener Response. Testen Sie CSS- und XPath-Selektoren in Echtzeit, bevor Sie sie zu Ihrem Spider hinzufügen:

>>> response.css('title::text').get()
'Example Domain'
>>> response.xpath('//h1/text()').get()
'Example Domain'

Scrapy Shell reduziert die Iterationszeit erheblich. Validieren Sie Extraktionslogik ohne vollständige Crawls auszuführen.

Vollständige Spider-Vorlage

Ein produktionsreifer Spider mit URL-Filterung, Status-Code-Behandlung und vollständiger SEO-Feldextraktion:

import re
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from urllib.parse import urlparse


class SEOSpider(CrawlSpider):
    name = "seospider"
    allowed_domains = ["example.com"]
    start_urls = ["https://www.example.com"]

    # Erfasse alle HTTP-Statuscodes, nicht nur 2xx
    handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]

    # Auszuschließende URL-Muster
    EXCLUDED_PATTERNS = re.compile(
        r"/(in|au|th|es|hk|sg|ph|my|ca|cn|uk|kr|id|fr|vn|de|jp|nl|it|tw)/"
    )

    rules = (
        Rule(
            LinkExtractor(allow=()),
            callback="parse_page",
            follow=True,
            process_links="filter_links",
        ),
    )

    def filter_links(self, links):
        filtered = []
        for link in links:
            parsed = urlparse(link.url)
            hostname = parsed.hostname or ""

            if hostname not in ("example.com", "www.example.com"):
                continue

            if self.EXCLUDED_PATTERNS.search(link.url):
                continue

            filtered.append(link)
        return filtered

    def parse_page(self, response):
        yield {
            "url": response.url,
            "status": response.status,
            "title": response.css("title::text").get(),
            "meta_description": response.css("meta[name='description']::attr(content)").get(),
            "meta_robots": response.css("meta[name='robots']::attr(content)").get(),
            "h1": response.css("h1::text").get(),
            "canonical": response.css("link[rel='canonical']::attr(href)").get(),
            "og_title": response.css("meta[property='og:title']::attr(content)").get(),
            "og_description": response.css("meta[property='og:description']::attr(content)").get(),
            "word_count": len(response.text.split()) if response.status == 200 else None,
            "content_type": response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore"),
        }

Ersetzen Sie example.com durch Ihre Zieldomain. Passen Sie EXCLUDED_PATTERNS an die URL-Struktur Ihrer Seite an.

Wann was verwenden

Screaming Frog:

Scrapy:

  • Seiten über 10.000 URLs
  • Automatisierte, geplante Crawls
  • Benutzerdefinierte Extraktionsanforderungen
  • CI/CD-Integration
  • Speicherbeschränkungen
  • Versionskontrollierte Konfigurationen

Das Fazit

Scrapy hat eine steilere Setup-Kurve als Screaming Frog, aber es beseitigt die praktischen Grenzen, die GUI-Crawler auferlegen. Keine URL-Limits, keine Lizenzgebühren, geringerer Speicherverbrauch und native Automatisierung.

Fangen Sie klein an. Crawlen Sie eine Seite, die Sie kennen. Verwenden Sie konservative Einstellungen. Vergleichen Sie die Ausgabe mit Screaming Frog. Die Daten werden übereinstimmen, aber Sie haben ein Tool, das skaliert.