Разработка краулера для сбора структуры сайтов конкурентов

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка краулера для сбора структуры сайтов конкурентов
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка краулера для сбора структуры сайтов конкурентов

Анализ структуры конкурирующих сайтов вручную — потеря времени при любом масштабе. Даже для среднего проекта в нише это 50–200 URL, которые надо не просто собрать, но и разобрать по уровням вложенности, якорям, мета-данным, схемам разметки. Написанный краулер решает это за минуты и воспроизводится при каждом ребрендинге конкурента.

Стек и подход

Два рабочих варианта: Python + Scrapy/Playwright для сложных SPA с ленивой загрузкой, Node.js + Puppeteer/Cheerio для большинства стандартных сайтов. В задачах, где нет динамического JS-рендеринга, хватает HTTP-клиента с HTML-парсером — быстрее в 5–10 раз, проще в деплое.

Минимальная Python-реализация на основе requests + lxml:

import requests
from lxml import html
from urllib.parse import urljoin, urlparse
from collections import deque
import time

class SiteStructureCrawler:
    def __init__(self, base_url: str, max_depth: int = 4, delay: float = 1.0):
        self.base_url = base_url
        self.domain = urlparse(base_url).netloc
        self.max_depth = max_depth
        self.delay = delay
        self.visited: dict[str, dict] = {}
        self.queue: deque = deque([(base_url, 0)])

    def crawl(self):
        session = requests.Session()
        session.headers['User-Agent'] = (
            'Mozilla/5.0 (compatible; SiteAnalyzer/1.0; +https://example.com/bot)'
        )

        while self.queue:
            url, depth = self.queue.popleft()
            if url in self.visited or depth > self.max_depth:
                continue

            try:
                resp = session.get(url, timeout=10, allow_redirects=True)
                resp.raise_for_status()
            except requests.RequestException as e:
                self.visited[url] = {'error': str(e), 'depth': depth}
                continue

            doc = html.fromstring(resp.content)
            doc.make_links_absolute(url)

            title = doc.findtext('.//title') or ''
            h1 = [h.text_content().strip() for h in doc.cssselect('h1')]
            meta_desc_el = doc.cssselect('meta[name="description"]')
            meta_desc = meta_desc_el[0].get('content', '') if meta_desc_el else ''
            canonical_el = doc.cssselect('link[rel="canonical"]')
            canonical = canonical_el[0].get('href', '') if canonical_el else ''
            noindex = bool(doc.cssselect('meta[name="robots"][content*="noindex"]'))

            links = []
            for a in doc.cssselect('a[href]'):
                href = a.get('href', '').strip()
                parsed = urlparse(href)
                if parsed.netloc == self.domain and href not in self.visited:
                    links.append(href)
                    if depth + 1 <= self.max_depth:
                        self.queue.append((href, depth + 1))

            self.visited[url] = {
                'depth': depth,
                'status': resp.status_code,
                'title': title.strip(),
                'h1': h1,
                'meta_description': meta_desc,
                'canonical': canonical,
                'noindex': noindex,
                'internal_links': links,
                'content_type': resp.headers.get('Content-Type', ''),
            }

            time.sleep(self.delay)

        return self.visited

Сбор дополнительных сигналов

Помимо базовой структуры, полезно собирать:

Schema.org разметка — тип страницы (Article, Product, BreadcrumbList, FAQPage), наличие structured data говорит о зрелости SEO-команды конкурента:

import json

def extract_schema(doc):
    schemas = []
    for script in doc.cssselect('script[type="application/ld+json"]'):
        try:
            data = json.loads(script.text_content())
            schemas.append(data)
        except json.JSONDecodeError:
            pass
    return schemas

Глубина заголовков — иерархия H1–H6 на страницах, типичные паттерны для категорийных страниц конкурента:

def heading_tree(doc):
    headings = []
    for tag in ['h1', 'h2', 'h3', 'h4']:
        for el in doc.cssselect(tag):
            headings.append({'tag': tag, 'text': el.text_content().strip()})
    return headings

Хлебные крошки — как конкурент строит навигацию и как это отражается в URL-структуре.

Работа с JavaScript-рендерингом

Если сайт конкурента — SPA (React/Vue/Angular) или использует lazy-load для основного контента, обычный HTTP-краулер вернёт пустые страницы. Здесь нужен headless-браузер:

from playwright.sync_api import sync_playwright

def crawl_spa_page(url: str) -> dict:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url, wait_until='networkidle', timeout=30000)

        title = page.title()
        h1_elements = page.query_selector_all('h1')
        h1_texts = [el.inner_text() for el in h1_elements]

        # Сбор всех ссылок после рендеринга
        links = page.eval_on_selector_all(
            'a[href]',
            'els => els.map(e => e.href)'
        )

        browser.close()
        return {'title': title, 'h1': h1_texts, 'links': links}

Playwright добавляет ~2–5 секунд на страницу против 0.3–0.8 секунды для обычного HTTP. При краулинге 500+ страниц это ощутимо — используется только там, где без него не обойтись.

Хранение и анализ результатов

Собранная структура экспортируется в несколько форматов в зависимости от задачи:

JSON — для дальнейшей программной обработки:

import json

with open('competitor_structure.json', 'w', encoding='utf-8') as f:
    json.dump(crawler.visited, f, ensure_ascii=False, indent=2)

CSV — для анализа в Excel/Google Sheets:

import csv

fieldnames = ['url', 'depth', 'status', 'title', 'meta_description', 'h1', 'noindex', 'canonical']

with open('competitor_structure.csv', 'w', newline='', encoding='utf-8') as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames, extrasaction='ignore')
    writer.writeheader()
    for url, data in crawler.visited.items():
        row = {'url': url, **data}
        if isinstance(row.get('h1'), list):
            row['h1'] = ' | '.join(row['h1'])
        writer.writerow(row)

SQLite — если нужно сравнивать несколько конкурентов или отслеживать изменения во времени:

import sqlite3

conn = sqlite3.connect('competitors.db')
conn.execute('''CREATE TABLE IF NOT EXISTS pages (
    url TEXT PRIMARY KEY,
    domain TEXT,
    depth INTEGER,
    status INTEGER,
    title TEXT,
    meta_description TEXT,
    noindex BOOLEAN,
    crawled_at DATETIME DEFAULT CURRENT_TIMESTAMP
)''')

Регулярный краулинг и diff

Разовый сбор данных быстро устаревает. Конкуренты меняют структуру, добавляют разделы, переформатируют заголовки. Полезно настроить автоматический запуск раз в неделю/месяц и сравнивать результаты:

def diff_structures(old: dict, new: dict) -> dict:
    added = {url: data for url, data in new.items() if url not in old}
    removed = {url: data for url, data in old.items() if url not in new}
    changed = {}
    for url in old:
        if url in new:
            if old[url].get('title') != new[url].get('title'):
                changed[url] = {
                    'old_title': old[url].get('title'),
                    'new_title': new[url].get('title'),
                }
    return {'added': added, 'removed': removed, 'changed': changed}

Этика и ограничения

Краулер должен уважать robots.txt. Библиотека robotparser из стандартной библиотеки Python:

from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url(f'{base_url}/robots.txt')
rp.read()

if not rp.can_fetch('*', url):
    continue  # пропускаем запрещённые пути

Задержка между запросами (delay) — обязательный параметр. Минимально разумное значение — 1 секунда. Для крупных сайтов лучше 2–3 секунды, чтобы не нагружать сервер и не попасть под бан по IP. Если краулинг нужен регулярно — имеет смысл ротация User-Agent и при необходимости прокси-пул.

Сроки

Базовый краулер (HTTP, без SPA) с экспортом в CSV/JSON — 1–2 рабочих дня. С поддержкой JavaScript-рендеринга, сбором Schema.org, дифф-сравнением и SQLite-хранилищем — 3–4 дня. Интеграция с планировщиком (cron/Airflow) и уведомлениями при изменениях — ещё 1–2 дня.