Screaming Frog це основний краулер для більшості SEO-спеціалістів, але ви, напевно, вже натрапляли на його обмеження: ліміт 500 URL у безкоштовній версії, переповнення RAM на великих сайтах, або бажання автоматизувати краули без нагляду за GUI. Scrapy це фреймворк Python з відкритим кодом, який усуває ці обмеження.

Якщо ви можете запустити npm install або git clone, ви можете запустити Scrapy. Крива навчання реальна, але керована, особливо якщо ви вже освоюєтесь з CLI-інструментами через агентні робочі процеси кодування.

Чому Scrapy?

Ключові переваги

Screaming Frog чудово працює для швидких аудитів. Але має обмеження:

Обмеження Вплив
Безкоштовний ліміт 500 URL Потрібна ліцензія $259/рік для більших сайтів
Пожирає пам’ять Великі краули можуть споживати 8GB+ RAM
Залежність від GUI Важко автоматизувати або планувати
Обмежена кастомізація Параметри конфігурації фіксовані

Scrapy вирішує ці проблеми:

Scrapy Що ви отримуєте
Безкоштовний і з відкритим кодом Без лімітів URL, без ліцензійних зборів
Менший обсяг пам’яті Черги на диску тримають RAM під контролем
CLI-нативний Скриптований, cron-сумісний, готовий до CI/CD
Повна Python-кастомізація Екстрактуйте що потрібно, фільтруйте як хочете
Призупинення/Відновлення Зупиняйте та продовжуйте великі краули будь-коли
Scrapy не замінить Screaming Frog для всього. Швидкі аудити все ще швидші в GUI. Але для масштабних краулів, автоматизації та кастомної екстракції варто мати його у своєму інструментарії.

Встановлення

Налаштування

Scrapy працює на Python. Використовуйте віртуальне середовище, щоб тримати все в чистоті:

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
Віртуальні середовища важливі
Завжди використовуйте venv. Глобальна установка спричиняє конфлікти залежностей і порушує відтворюваність.

Створення проекту

Зі встановленим Scrapy:

scrapy startproject myproject
cd myproject
scrapy genspider sitename example.com

Це створює:

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

Код спайдера йде в spiders/sitename.py. Конфігурація знаходиться в settings.py.

Налаштування для ввічливого краулінгу

Критично

Налаштуйте settings.py перед запуском чого-небудь. Блокування витрачає більше часу, ніж повільний краулінг.

# Ввічливий краулінг
CONCURRENT_REQUESTS_PER_DOMAIN = 5
DOWNLOAD_DELAY = 1
ROBOTSTXT_OBEY = True

# AutoThrottle - регулює швидкість на основі відповіді сервера
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
AUTOTHROTTLE_DEBUG = True

# Ліміти безпеки
CLOSESPIDER_PAGECOUNT = 10000

# Вивід
FEED_EXPORT_ENCODING = "utf-8"
З увімкненим AutoThrottle, 5 одночасних запитів, це розумна початкова точка. AutoThrottle автоматично відступить, якщо сервер має проблеми. Без AutoThrottle починайте нижче, з 1-3.

AutoThrottle

AutoThrottle моніторить час відповіді сервера та автоматично регулює швидкість краулу:

  • Швидкі відповіді → прискорює
  • Повільні відповіді → уповільнює
  • Помилки/таймаути → значно уповільнює

На відміну від фіксованих затримок Screaming Frog, він адаптується до реальних умов сервера.

Обробка кодів статусу

За замовчуванням HttpErrorMiddleware Scrapy тихо відкидає відповіді не-2xx. Це означає, що 404, 301, 500 відкидаються до того, як досягнуть вашого callback. Ваш краул може показувати 100% кодів статусу 200, не тому, що сайт ідеальний, а тому, що помилки фільтруються.

Додайте це до вашого класу спайдера, щоб захоплювати всі коди статусу:

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

Screaming Frog за замовчуванням захоплює всі коди статусу. Це налаштування приводить Scrapy у відповідність з цією поведінкою.

Продуктивність у реальному світі

Бенчмарки

Реальні цифри з тестового краулу з 5 одночасними запитами та увімкненим AutoThrottle:

Прогрес краулу Сторінок/Хвилина Примітки
0-200 сторінок 14-22 Розгін
200-500 сторінок 10-12 Стабілізація
500-1000 сторінок 7-10 AutoThrottle регулює
1000+ сторінок 5-7 Стабільний стан
Швидкість vs. Надійність
Ці швидкості виглядають повільними. В цьому суть. AutoThrottle надає пріоритет здоров'ю сервера над сирою швидкістю. Блокування та перезапуск витрачають більше часу, ніж методичний краул.

Порівняння функцій

Функція Screaming Frog Scrapy
Вартість Безкоштовно <500 URL, ~$259/рік Безкоштовно, відкритий код
Макс. розмір краулу Обмежено пам’яттю Черги на диску
Кастомізація Обмежені опції конфігурації Повний Python-код
Планування Вручну або сторонні Нативний CLI, cron-сумісний
Призупинення/Відновлення Так Так (з JOBDIR)
Крива навчання Низька (GUI) Середня (код)
Обмеження швидкості Базові фіксовані затримки AutoThrottle (адаптивний)
Рендеринг JavaScript Опціонально (Chrome) Опціонально (playwright/splash)
Коди статусу Всі за замовчуванням Потребує налаштування
Фільтрація піддоменів GUI чекбокси Код (гнучкий regex)
Формати експорту CSV, Excel тощо JSON, CSV, XML, кастомний
CI/CD інтеграція Складна Нативна

Фільтрація URL

Точний контроль

Screaming Frog використовує чекбокси. Scrapy використовує код. Компроміс: крива навчання за точність.

Виключення міжнародних шляхів:

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/"]

    # Пропускаємо міжнародні шляхи типу /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

Ви можете фільтрувати за URL-патернами, параметрами запиту, заголовками відповіді, вмістом сторінки або будь-якою комбінацією.

Інтеграція Sitemap

Виявлення URL

Screaming Frog має простий чекбокс “використовувати sitemap”. Scrapy вимагає кастомного коду, але дає повний контроль над тим, як sitemap парситься та інтегрується з вашим краулом.

Навіщо додавати підтримку sitemap?

  • Виявляє URL-адреси, не пов’язані з головної навігації
  • Знаходить сирітські сторінки, які краулінг за посиланнями пропустив би
  • Отримує “офіційний” список URL-адрес сайту для порівняння
  • Може виявити більше сторінок, ніж слідування за посиланнями
  • Необхідно для повних SEO-аудитів

Додайте ці методи до вашого CrawlSpider для увімкнення виявлення та парсингу 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}")

Як це працює:

  1. start_requests() перевизначає стандартну поведінку для спочатку отримання sitemap
  2. parse_robots() знаходить рядки Sitemap: у robots.txt
  3. parse_sitemap() обробляє як індекси sitemap, так і звичайні sitemap
  4. XPath //sitemap/loc знаходить вкладені sitemap у файлах індексу
  5. XPath //url/loc знаходить фактичні URL-адреси сторінок
  6. Та сама фільтрація домену та патернів застосовується до URL-адрес sitemap
  7. Вбудована дедуплікація Scrapy запобігає подвійному краулінгу сторінок, знайдених як у sitemap, так і за посиланнями
Функція Screaming Frog Scrapy
Виявлення sitemap Чекбокс Кастомний код
Парсинг robots.txt Автоматичний Кастомний код
Підтримка індексу sitemap Так Так (з кодом)
Фільтрація URL Опції GUI Код (повний контроль)
Об’єднання з краулом Так Так
Кастомні розташування sitemap Ручне введення Код будь-якого розташування

З інтеграцією sitemap ви можете виявити сирітські сторінки, не пов’язані з навігації, старий архівний контент, все ще присутній у sitemap, варіації URL зі слешем або без нього на кінці, та сторінки, заблоковані robots.txt, але все ще присутні в sitemap. Це дає повнішу картину сайту для SEO-аудитів.

Призупинення та відновлення

Необхідно

Для краулів понад 1000 сторінок увімкніть призупинення/відновлення з JOBDIR:

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

Scrapy зберігає стан у crawl_state/. Натисніть Ctrl+C для призупинення. Запустіть ту саму команду для відновлення.

Завжди використовуйте JOBDIR для продакшн краулів. Захищає від мережевих проблем, перезапусків системи або просто необхідності зупинитись на день.

Стан включає URL в очікуванні, переглянуті URL та чергу запитів. Це надійніше, ніж функція збереження/завантаження Screaming Frog, оскільки базується на файлах і витримує перезапуски системи.

Рендеринг JavaScript

Scrapy отримує лише сирий HTML. Він не рендерить JavaScript. Це те саме, що повертає curl.

Для більшості SEO-краулів це нормально:

  • Мета-теги, canonical та h1 зазвичай в початковому HTML
  • Пошукові системи переважно індексують серверний контент
  • Більшість e-commerce та контент-сайтів рендеряться на сервері

Якщо ваш цільовий сайт рендерить контент на стороні клієнта, є варіанти:

Пакет Примітки
scrapy-playwright Використовує Chromium/Firefox/WebKit. Рекомендовано для сучасних JS-сайтів
scrapy-splash Легкий, Docker-based рендерер
scrapy-selenium Старіший підхід, все ще працює

JS-рендеринг значно повільніший та більш ресурсомісткий. Додавайте лише якщо сайт цього потребує.

Screaming Frog має подібний компроміс. Увімкнення рендерингу JavaScript використовує Chrome під капотом і значно уповільнює краули.

Управління пам’яттю

На ~1300 сторінках з повною екстракцією полів:

  • Пам’ять: ~265 МБ
  • CPU: ~4%

Використання JOBDIR переносить черги запитів на диск, тримаючи пам’ять низькою. Для дуже великих краулів (100k+ URL) додайте ці налаштування:

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

Це обмежує використання пам’яті та примушує дискові черги для планувальника.

Вивід даних

Налаштовуваний

Базовий вивід спайдера:

{
    "url": "https://www.example.com/page/",
    "title": "Заголовок сторінки тут",
    "status": 200
}

Для SEO-краулів вам потрібні поля, подібні до того, що експортує 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"),
    }

Додавайте або видаляйте поля залежно від потреб. CSS-селектори працюють для будь-якого елемента на сторінці.

Формати експорту: JSON (-o output.json), JSON Lines (-o output.jsonl), CSV (-o output.csv), XML (-o output.xml).

JSON Lines найкращий для великих краулів. Файли валідні рядок за рядком під час краулу, тому ви можете моніторити за допомогою tail -f. Стандартний JSON не валідний, поки краул не завершиться.

Screaming Frog → Scrapy

Посібник з перекладу

Відображення робочих процесів SF на Scrapy:

Дія Screaming Frog Еквівалент Scrapy
Почати новий краул scrapy crawl spidername
Встановити затримку краулу DOWNLOAD_DELAY у налаштуваннях
Обмежити одночасні потоки CONCURRENT_REQUESTS_PER_DOMAIN
Дотримуватись robots.txt ROBOTSTXT_OBEY = True
Експортувати в CSV -o output.csv
Зберегти/Завантажити краул -s JOBDIR=crawl_state
Фільтрувати піддомени Код у спайдері (regex)
Кастомна екстракція CSS/XPath селектори в parse()

Зміни мислення:

  1. Конфігурація це код. Редагуйте settings.py замість клікання чекбоксів.
  2. Екстракція явна. Ви пишете, які дані захоплювати.
  3. Планування нативне. Додавайте команди до cron або CI/CD.
  4. Дебагінг це логи. Увімкніть AUTOTHROTTLE_DEBUG, щоб бачити, що відбувається.

Повний робочий процес

Зі стандартними налаштуваннями вище ви можете мати Scrapy встановленим і краулити менш ніж за 15 хвилин:

python3 -m venv venv
source venv/bin/activate  # venv\Scripts\activate на Windows
pip install scrapy
scrapy startproject urlcrawler
cd urlcrawler
scrapy genspider mysite example.com
# Відредагуйте settings.py з конфігурацією ввічливого краулу
# Відредагуйте spiders/mysite.py з вашою логікою parse
scrapy crawl mysite -o urls.jsonl -s JOBDIR=crawl_state

Scrapy Shell

Створюючи кастомні конфігурації, використовуйте Scrapy Shell для інтерактивного тестування селекторів та налаштувань:

scrapy shell "https://example.com"

Це відкриває інтерактивну Python-консоль з уже завантаженою відповіддю. Тестуйте CSS та XPath селектори в реальному часі перед додаванням їх до спайдера:

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

Scrapy Shell значно скорочує час ітерації. Валідуйте логіку екстракції без запуску повних краулів.

Повний шаблон спайдера

Готовий до продакшн спайдер з фільтрацією URL, обробкою кодів статусу та повною екстракцією 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"),
        }

Замініть example.com на ваш цільовий домен. Налаштуйте EXCLUDED_PATTERNS для структури URL вашого сайту.

Коли що використовувати

Screaming Frog:

Scrapy:

  • Сайти понад 10 000 URL
  • Автоматизовані, заплановані краули
  • Потреби в кастомній екстракції
  • CI/CD інтеграція
  • Обмеження пам’яті
  • Версіоновані конфігурації

Підсумок

Scrapy має крутішу криву налаштування, ніж Screaming Frog, але усуває практичні обмеження, які накладають GUI-краулери. Без лімітів URL, без ліцензійних зборів, нижче використання пам’яті та нативна автоматизація.

Почніть з малого. Прокраульте сайт, який знаєте. Використовуйте консервативні налаштування. Порівняйте вивід зі Screaming Frog. Дані збігатимуться, але у вас буде інструмент, який масштабується.