Screaming Frog to główny crawler dla większości SEO, ale prawdopodobnie napotkałeś jego ograniczenia: limit 500 URL w wersji darmowej, maksymalne wykorzystanie RAM na dużych witrynach, lub chęć automatyzacji crawli bez pilnowania GUI. Scrapy to open source’owy framework Python, który usuwa te limity.
Jeśli możesz uruchomić npm install lub git clone, możesz uruchomić Scrapy. Krzywa uczenia jest realna, ale do opanowania, szczególnie jeśli już oswajasz się z narzędziami CLI poprzez agentyczne workflow kodowania.
Dlaczego Scrapy?
Screaming Frog działa świetnie do szybkich audytów. Ale ma limity:
| Ograniczenie | Wpływ |
|---|---|
| Limit 500 URL za darmo | Wymaga licencji $259/rok dla większych witryn |
| Żarłoczny na pamięć | Duże crawle mogą zużywać 8GB+ RAM |
| Zależny od GUI | Trudny do automatyzacji lub harmonogramowania |
| Ograniczona personalizacja | Opcje konfiguracji są stałe |
Scrapy rozwiązuje te problemy:
| Scrapy | Co otrzymujesz |
|---|---|
| Darmowy i open source | Bez limitów URL, bez opłat licencyjnych |
| Niższy ślad pamięciowy | Kolejki na dysku utrzymują RAM pod kontrolą |
| Natywny CLI | Skryptowalny, cronowalny, gotowy na CI/CD |
| Pełna personalizacja Python | Ekstrahuj co potrzebujesz, filtruj jak chcesz |
| Wstrzymywanie/Wznawianie | Zatrzymuj i kontynuuj duże crawle w dowolnym momencie |
Instalacja
Scrapy działa na Pythonie. Użyj wirtualnego środowiska, aby zachować porządek:
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
Tworzenie projektu
Z zainstalowanym Scrapy:
scrapy startproject myproject
cd myproject
scrapy genspider sitename example.com
To tworzy:
myproject/
scrapy.cfg
myproject/
__init__.py
items.py
middlewares.py
pipelines.py
settings.py
spiders/
__init__.py
sitename.py
Kod spidera trafia do spiders/sitename.py. Konfiguracja znajduje się w settings.py.
Ustawienia dla uprzejmego crawlowania
Skonfiguruj settings.py przed uruchomieniem czegokolwiek. Zablokowanie marnuje więcej czasu niż powolne crawlowanie.
# Uprzejme crawlowanie
CONCURRENT_REQUESTS_PER_DOMAIN = 5
DOWNLOAD_DELAY = 1
ROBOTSTXT_OBEY = True
# AutoThrottle - dostosowuje prędkość na podstawie odpowiedzi serwera
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = True
# Limity bezpieczeństwa
CLOSESPIDER_PAGECOUNT = 10000
# Wyjście
FEED_EXPORT_ENCODING = "utf-8"
AutoThrottle
AutoThrottle monitoruje czasy odpowiedzi serwera i automatycznie dostosowuje prędkość crawla:
- Szybkie odpowiedzi → przyspiesza
- Wolne odpowiedzi → zwalnia
- Błędy/timeout → znacząco zwalnia
W przeciwieństwie do stałych opóźnień Screaming Frog, adaptuje się do rzeczywistych warunków serwera.
Obsługa kodów statusu
Domyślnie HttpErrorMiddleware Scrapy cicho odrzuca odpowiedzi inne niż 2xx. Oznacza to, że 404, 301, 500 są odrzucane przed dotarciem do twojego callbacka. Twój crawl może pokazywać 100% kodów statusu 200, nie dlatego, że strona jest idealna, ale dlatego, że błędy są filtrowane.
Dodaj to do swojej klasy spidera, aby przechwytywać wszystkie kody statusu:
handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]
Screaming Frog domyślnie przechwytuje wszystkie kody statusu. To ustawienie dopasowuje Scrapy do tego zachowania.
Wydajność w rzeczywistości
Rzeczywiste liczby z testowego crawla z 5 równoczesnymi żądaniami i włączonym AutoThrottle:
| Postęp crawla | Strony/Minutę | Uwagi |
|---|---|---|
| 0-200 stron | 14-22 | Rozruch |
| 200-500 stron | 10-12 | Stabilizacja |
| 500-1000 stron | 7-10 | AutoThrottle dostosowuje |
| 1000+ stron | 5-7 | Stan ustalony |
Porównanie funkcji
| Funkcja | Screaming Frog | Scrapy |
|---|---|---|
| Koszt | Darmowy <500 URL, ~$259/rok | Darmowy, open source |
| Maks. rozmiar crawla | Ograniczony pamięcią | Kolejki na dysku |
| Personalizacja | Ograniczone opcje konfiguracji | Pełny kod Python |
| Harmonogramowanie | Ręczne lub zewnętrzne | Natywny CLI, cronowalny |
| Wstrzymywanie/Wznawianie | Tak | Tak (z JOBDIR) |
| Krzywa uczenia | Niska (GUI) | Średnia (kod) |
| Ograniczanie szybkości | Podstawowe stałe opóźnienia | AutoThrottle (adaptacyjny) |
| Renderowanie JavaScript | Opcjonalne (Chrome) | Opcjonalne (playwright/splash) |
| Kody statusu | Wszystkie domyślnie | Wymaga konfiguracji |
| Filtrowanie subdomen | Checkboxy GUI | Kod (elastyczne regex) |
| Formaty eksportu | CSV, Excel, itp. | JSON, CSV, XML, niestandardowy |
| Integracja CI/CD | Trudna | Natywna |
Filtrowanie URL
Screaming Frog używa checkboxów. Scrapy używa kodu. Kompromis to krzywa uczenia za precyzję.
Wykluczanie międzynarodowych ścieżek:
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/"]
# Pomiń międzynarodowe ścieżki jak /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
Możesz filtrować według wzorców URL, parametrów zapytania, nagłówków odpowiedzi, zawartości strony lub dowolnej kombinacji.
Integracja Sitemap
Screaming Frog ma prosty checkbox “użyj sitemap”. Scrapy wymaga niestandardowego kodu, ale daje pełną kontrolę nad sposobem parsowania i integracji sitemap z crawlem.
Dlaczego warto dodać obsługę sitemap?
- Odkrywa URL-e niepodlinkowane z głównej nawigacji
- Znajduje osierocone strony, które crawlowanie oparte na linkach by pominęło
- Pobiera “oficjalną” listę URL-ów witryny do porównania
- Może odkryć więcej stron niż samo podążanie za linkami
- Niezbędne dla kompletnych audytów SEO
Dodaj te metody do swojego CrawlSpider, aby włączyć wykrywanie i parsowanie sitemap:
def start_requests(self):
# First, fetch robots.txt to find sitemaps
yield Request(
"https://www.example.com/robots.txt",
callback=self.parse_robots,
errback=self.handle_error,
dont_filter=True,
)
# Also try common sitemap locations directly
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},
)
# Also start normal crawl from homepage
for url in self.start_urls:
yield Request(url, callback=self.parse_start_url)
def parse_robots(self, response):
"""Parse robots.txt to find sitemap declarations"""
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"Found sitemap in robots.txt: {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 or sitemap index"""
if response.status != 200:
return
content_type = response.headers.get("Content-Type", b"").decode("utf-8", errors="ignore")
# Check if this is XML content
if "xml" not in content_type and not response.text.strip().startswith("<?xml"):
return
# Check for sitemap index (contains other sitemaps)
sitemap_locs = response.xpath("//sitemap/loc/text()").getall()
if sitemap_locs:
self.logger.info(f"Found sitemap index with {len(sitemap_locs)} sitemaps")
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},
)
# Parse URL entries from sitemap
url_locs = response.xpath("//url/loc/text()").getall()
if url_locs:
self.logger.info(f"Found {len(url_locs)} URLs in sitemap: {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):
"""Handle the start URL and trigger rules"""
yield from self.parse_page(response)
yield from self._requests_to_follow(response)
def is_valid_url(self, url):
"""Check if URL is valid and within allowed domains"""
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):
"""Apply the same filtering as filter_links"""
if self.EXCLUDED_PATTERNS.search(url):
return False
return True
def handle_error(self, failure):
"""Handle request errors gracefully"""
self.logger.warning(f"Request failed: {failure.request.url}")
Jak to działa:
start_requests()nadpisuje domyślne zachowanie, aby najpierw pobrać sitemapyparse_robots()znajduje linieSitemap:w robots.txtparse_sitemap()obsługuje zarówno indeksy sitemap, jak i zwykłe sitemapy- XPath
//sitemap/locznajduje zagnieżdżone sitemapy w plikach indeksu - XPath
//url/locznajduje rzeczywiste URL-e stron - To samo filtrowanie domen i wzorców dotyczy URL-i z sitemap
- Wbudowana deduplikacja Scrapy zapobiega podwójnemu crawlowaniu stron znalezionych zarówno w sitemap, jak i linkach
| Funkcja | Screaming Frog | Scrapy |
|---|---|---|
| Wykrywanie sitemap | Checkbox | Niestandardowy kod |
| Parsowanie robots.txt | Automatyczne | Niestandardowy kod |
| Obsługa indeksu sitemap | Tak | Tak (z kodem) |
| Filtrowanie URL | Opcje GUI | Kod (pełna kontrola) |
| Scalanie z crawlem | Tak | Tak |
| Niestandardowe lokalizacje sitemap | Ręczne wprowadzanie | Kod dowolnej lokalizacji |
Dzięki integracji sitemap możesz odkryć osierocone strony niepodlinkowane z nawigacji, stare zarchiwizowane treści wciąż obecne w sitemap, warianty URL z ukośnikiem końcowym lub bez, oraz strony zablokowane przez robots.txt, ale wciąż obecne w sitemap. Daje to pełniejszy obraz witryny dla audytów SEO.
Wstrzymywanie i wznawianie
Dla crawli powyżej 1000 stron, włącz wstrzymywanie/wznawianie z JOBDIR:
scrapy crawl myspider -o output.json -s JOBDIR=crawl_state
Scrapy zapisuje stan do crawl_state/. Naciśnij Ctrl+C, aby wstrzymać. Uruchom to samo polecenie, aby wznowić.
Stan zawiera oczekujące URL-e, widziane URL-e i kolejkę żądań. Jest to bardziej solidne niż funkcja zapisz/wczytaj Screaming Frog, ponieważ jest oparta na plikach i przetrwa restarty systemu.
Renderowanie JavaScript
Scrapy pobiera tylko surowy HTML. Nie renderuje JavaScript. To jest to samo, co zwraca curl.
Dla większości crawli SEO to jest w porządku:
- Tagi meta, canonicale i h1 są zwykle w początkowym HTML
- Wyszukiwarki głównie indeksują zawartość renderowaną po stronie serwera
- Większość witryn e-commerce i content jest renderowana po stronie serwera
Jeśli twoja docelowa witryna renderuje zawartość po stronie klienta, masz opcje:
| Pakiet | Uwagi |
|---|---|
| scrapy-playwright | Używa Chromium/Firefox/WebKit. Zalecany dla nowoczesnych stron JS |
| scrapy-splash | Lekki, renderer oparty na Docker |
| scrapy-selenium | Starsze podejście, nadal działa |
Renderowanie JS jest znacznie wolniejsze i bardziej zasobożerne. Dodawaj je tylko jeśli strona tego wymaga.
Screaming Frog ma podobny kompromis. Włączenie renderowania JavaScript używa Chrome pod spodem i znacząco spowalnia crawle.
Zarządzanie pamięcią
Przy ~1300 stronach z pełną ekstrakcją pól:
- Pamięć: ~265 MB
- CPU: ~4%
Używanie JOBDIR przenosi kolejki żądań na dysk, utrzymując niskie zużycie pamięci. Dla bardzo dużych crawli (100k+ URL), dodaj te ustawienia:
MEMUSAGE_LIMIT_MB = 1024
MEMUSAGE_WARNING_MB = 800
SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleFifoDiskQueue'
SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.FifoMemoryQueue'
To ogranicza zużycie pamięci i wymusza kolejki oparte na dysku dla schedulera.
Dane wyjściowe
Podstawowe wyjście spidera:
{
"url": "https://www.example.com/page/",
"title": "Tytuł strony tutaj",
"status": 200
}
Dla crawli SEO potrzebujesz pól podobnych do tego, co eksportuje Screaming Frog:
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"),
}
Dodawaj lub usuwaj pola w zależności od potrzeb. Selektory CSS działają dla każdego elementu na stronie.
Formaty eksportu: JSON (-o output.json), JSON Lines (-o output.jsonl), CSV (-o output.csv), XML (-o output.xml).
JSON Lines jest najlepszy dla dużych crawli. Pliki są ważne linia po linii podczas crawla, więc możesz monitorować za pomocą tail -f. Standardowy JSON nie jest ważny, dopóki crawl się nie zakończy.
Screaming Frog → Scrapy
Mapowanie workflow SF na Scrapy:
| Akcja Screaming Frog | Odpowiednik Scrapy |
|---|---|
| Rozpocznij nowy crawl | scrapy crawl spidername |
| Ustaw opóźnienie crawla | DOWNLOAD_DELAY w ustawieniach |
| Ogranicz równoczesne wątki | CONCURRENT_REQUESTS_PER_DOMAIN |
| Respektuj robots.txt | ROBOTSTXT_OBEY = True |
| Eksportuj do CSV | -o output.csv |
| Zapisz/Wczytaj crawl | -s JOBDIR=crawl_state |
| Filtruj subdomeny | Kod w spiderze (regex) |
| Niestandardowa ekstrakcja | Selektory CSS/XPath w parse() |
Zmiany w myśleniu:
- Konfiguracja to kod. Edytuj
settings.pyzamiast klikać checkboxy. - Ekstrakcja jest jawna. Piszesz, jakie dane przechwycić.
- Harmonogramowanie jest natywne. Dodaj polecenia do crona lub CI/CD.
- Debugowanie to logi. Włącz
AUTOTHROTTLE_DEBUG, aby zobaczyć, co się dzieje.
Pełny workflow
Z powyższymi standardowymi ustawieniami możesz mieć Scrapy zainstalowane i crawlujące w mniej niż 15 minut:
python3 -m venv venv
source venv/bin/activate # venv\Scripts\activate na Windows
pip install scrapy
scrapy startproject urlcrawler
cd urlcrawler
scrapy genspider mysite example.com
# Edytuj settings.py z konfiguracją uprzejmego crawla
# Edytuj spiders/mysite.py z logiką parse
scrapy crawl mysite -o urls.jsonl -s JOBDIR=crawl_state
Scrapy Shell
Budując niestandardowe konfiguracje, użyj Scrapy Shell do interaktywnego testowania selektorów i ustawień:
scrapy shell "https://example.com"
To otwiera interaktywną konsolę Python z już załadowaną odpowiedzią. Testuj selektory CSS i XPath w czasie rzeczywistym przed dodaniem ich do spidera:
>>> response.css('title::text').get()
'Example Domain'
>>> response.xpath('//h1/text()').get()
'Example Domain'
Scrapy Shell znacząco skraca czas iteracji. Waliduj logikę ekstrakcji bez uruchamiania pełnych crawli.
Kompletny szablon spidera
Produkcyjny spider z filtrowaniem URL, obsługą kodów statusu i pełną ekstrakcją pól SEO:
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"]
# Capture all HTTP status codes, not just 2xx
handle_httpstatus_list = [200, 301, 302, 403, 404, 500, 502, 503]
# URL patterns to exclude
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"),
}
Zamień example.com na swoją docelową domenę. Dostosuj EXCLUDED_PATTERNS do struktury URL twojej witryny.
Kiedy używać którego
Screaming Frog:
- Szybkie audyty poniżej 500 URL
- Wyniki potrzebne w minutach
- Wizualna eksploracja witryny
- Nie czujesz się komfortowo z CLI
- Używanie danych Screaming Frog z Redirects.net
Scrapy:
- Witryny powyżej 10 000 URL
- Zautomatyzowane, zaplanowane crawle
- Niestandardowe potrzeby ekstrakcji
- Integracja CI/CD
- Ograniczenia pamięci
- Wersjonowane konfiguracje
Podsumowanie
Scrapy ma bardziej stromą krzywą konfiguracji niż Screaming Frog, ale usuwa praktyczne limity, które narzucają crawlery GUI. Bez limitów URL, bez opłat licencyjnych, niższe zużycie pamięci i natywna automatyzacja.
Zacznij od małego. Zcrawluj witrynę, którą znasz. Użyj konserwatywnych ustawień. Porównaj wyjście ze Screaming Frog. Dane będą się zgadzać, ale będziesz mieć narzędzie, które się skaluje.