ADR-0027: HTML scraper connector pattern (Pattern 4)

Status: accepted Date: 2026-04-18 Deciders: команда проекта

Контекст

Второй поставщик (smart-shop.pro) не имеет публичного API. Данные доступны только через HTML страницы с server-side markup + client-side AJAX фильтрацию. Current integration patterns в ../integration-patterns.md:

  • Pattern 1 — REST API.
  • Pattern 2 — File feed (FTP / S3 / HTTP).
  • Pattern 3 — Webhook push.

HTML scraping не покрывается ни одним. Попытка натянуть Pattern 1 приводит к:

  • неструктурированному RawPayload (HTML blob вместо JSON);
  • ручному парсингу без контрактного схемы;
  • rate limits, специфичных для сайта (скрапер часто banится за 1-2 req/сек, когда REST держит 10+);
  • необходимости CSR rendering (Chromium), что ломает assumption «HTTP-клиент достаточен».

Есть юридические и этические риски: scraping может нарушать TOS поставщика. Нужен формальный процесс допуска таких connector’ов + технический паттерн.

Решение

Pattern 4: HTML scraper connector — четвёртый стандартный паттерн. Реализация следует общему Connector interface (ADR-0024), но имеет специфическую инфраструктуру и compliance-требования.

1. Допуск connector’а — compliance gate

Перед началом implementation — обязательные чеки. Composition root отказывается поднимать scraper-connector, если любой из пунктов не удовлетворён:

ЧекАртефакт
Проверить robots.txt целевого сайта — запрещены ли нужные пути?compliance/robots-check.yaml + output cron-теста.
Прочитать TOS — явный запрет автоматического сбора данных?compliance/tos-analysis.md (юр-заключение или комментарий product-ответственного).
Получить письменное разрешение поставщика (email / договор) — если TOS запрещает или даже неоднозначен.compliance/written-permission.md с прикреплённым оригиналом.
Согласовать User-Agent: мы идентифицируемся, не маскируемся под browser.EgressPolicy.user_agent_signature обязательное поле.
Определить rate budget consistent с ethical crawling (start conservative: ≤ 0.5 req/сек, дальше по согласованию).rate_limit.data.*.rate в конфиге.
Согласовать Retry-After compliance: при любом 429/503 — ждать указанный timeout, не игнорировать.Стандартное поведение RateLimiter + connector обязан эмитить ConnectorError.RateLimited с Retry-After.

Нарушение любого пункта — merge connector’а блокируется CI (dedicated compliance-gate stage).

2. Transport layer

Capabilities.TransportKind enum добавляется (ADR-0024):

TransportKind:
  rest
  file_feed
  webhook
  html_scrape

Capabilities.RequiresHeadlessBrowser: bool — если сайт client-side rendered и нужен Chromium. Влияет на deploy: pod требует ≥ 1Gi RAM и специальный base image.

Capabilities.RespectsRobotsTxt: bool = true — всегда true для html_scrape. Если explicitly false — требуется ADR-override (например, permission в written-agreement).

3. SessionManager для scraper’а

Вместо session-key API — cookie jar + CSRF:

ScraperSession {
  cookies: http.CookieJar (encrypted at rest)
  csrf_token: string?
  logged_in_as: string
  acquired_at, valid_until
  user_agent: string           // фиксируем на session'е
}

Login — через POST /login с form data. Успех определяется по set-cookie + absence of login-form на return page. Token refresh — по heartbeat-запросу к pricing-free page каждые N минут.

4. Fetch layer (без CSR)

Для сайтов с SSR (полный HTML возвращается сервером):

  • net/http клиент с cookie jar.
  • gzip / brotli decode.
  • Encoding detection (у РФ-сайтов типично windows-1251, иногда с BOM).
  • Retry policy: Transient — exp backoff; 429 / 503 + Retry-After — wait-and-retry; 403 / 401 — re-login один раз.
  • Pagination walker через ?page=N.

5. Fetch layer (с CSR — headless)

Для smart-shop.pro и подобных, где товары / цены рендерятся JS:

  • Pod с Chromium (через Playwright Go / chromedp).
  • Policy: одна session на один pod; каждый Connector.Fetch переиспользует session, новые pages — в новой вкладке.
  • Timeout на page load: 30 сек.
  • Resource budgets: block image / media / font requests по умолчанию — экономит bandwidth и rate.
  • Screenshot on failure → DLQ artifact (для postmortem сложных 403’ок).
  • Важно: headless браузер — отдельный process, падение не роняет ingestor основной. Pod имеет liveness probe на Chromium health.

Конфигурация:

connectors:
  smart-shop-pro:
    transport: html_scrape
    renders_csr: true
    headless:
      image: mcr.microsoft.com/playwright/go:latest
      cpu: 500m
      memory: 1Gi
      page_timeout: 30s
      block_resource_types: [image, font, media, stylesheet]

6. Parser layer

Парсер HTML → RawPayload имеет двойную ответственность:

  • Извлечение структурированных данных (CSS selectors / XPath).
  • Нормализация encoding, decimal separators, null-vs-empty-string semantics.

Рекомендация: parser tests на golden HTML fixtures (снимки реальных страниц). Изменение markup поставщика → падение теста → alert. Без этого scraper разваливается молча при первом редизайне сайта.

SelectorRegistry — отдельный файл per-connector:

smart-shop-pro.selectors.yaml:
  listing:
    item_row: "div.product-row"
    item_title: "a.product-name"
    item_article: ".product-article"
    item_price: ".price-value"
    item_sellers: ".seller-offer-row"
  itemcard:
    title: "h1.product-title"
    characteristics_table: "table.product-characteristics tr"
    seller_row: ".seller-info"
    seller_rating: ".seller-rating .score"

При breaking redesign — меняем только yaml, не код.

7. HTML markup drift detection

Parser сравнивает структурные markers с baseline:

  • количество item_row на listing page ≥ min_threshold;
  • presence обязательных полей (price, article, title);
  • regex-match на price formats.

При drift — ConnectorError.Permanent + alert connector_markup_drift_total{supplier}. НЕ пытаемся парсить с guessing — fail-fast, DLQ, operator reviews.

8. Ethical crawling

  • Identified User-Agent: Tracium-Bot/1.0 (+https://tracium.tld/bot; contact=catalog-ops@tracium.tld).
  • Conditional requests: If-Modified-Since / ETag где поддерживается.
  • No parallel crawl within same host: один worker per host, не несколько параллельных TCP-соединений.
  • Sitemap / sitemap.xml — предпочитать, если есть (получаем список URL без crawl’а).
  • Skip auth-walls, где TOS неоднозначен. Поведение customer credential — только через явный consent customer’а (customer сам ввёл свой логин/пароль; мы используем только от его имени).
  • Honor 429 / 503 / Retry-After: никаких «а вдруг отдаст, попробуем ещё раз через секунду».

9. Rate budget (двухуровневый, ADR-0008)

AuthBucket (login) и DataBucket[endpoint_class] остаются. Типовые стартовые значения для scraper’а:

rate_limit:
  auth:
    login:
      rate: 1
      period: 300s          # 5 минут между login'ами — агрессивно консервативно
      mode: wait
  data:
    listing:
      rate: 1
      period: 2s
      mode: skip_and_reschedule
    itemcard:
      rate: 1
      period: 2s
      mode: skip_and_reschedule
    search:
      rate: 1
      period: 3s
      mode: skip_and_reschedule

После согласования с поставщиком — можно увеличить.

10. Data quality degraded mode

Scraping менее надёжен, чем API. Признаки:

  • не все seller’ы видны без авторизации;
  • цены могут быть A/B-тестированы (meaningfully different на разных сессиях);
  • markup drift — временные artefacts;
  • иногда rendering зависает, возвращается частичная страница.

Поэтому observations от scraper-connector’а помечаются observation_source.reliability = degraded (опциональное поле). Pricing tie-break: при равных условиях observations от REST-API источников предпочитаются.

Последствия

Плюсы

  • Legitimate path для добавления поставщиков без API — расширяет охват каталога.
  • Formal compliance gate защищает от юр-рисков.
  • Parser tests на golden fixtures делают scraper maintainable.
  • Headless browser изолирован (отдельный pod) — падение не затрагивает ingestor core.

Минусы

  • Scraper-connector’ы дороже в эксплуатации: Chromium pod, частая поломка от redesign, ручные compliance-процедуры.
  • Data quality ниже: reliability=degraded становится нормой.
  • Scaling ограничено: headless pod плохо масштабируется, 1 worker per host invariant.

Нейтральные последствия

  • Любой scraper-connector может быть «выключен» без потери core-данных (merely поставщик исчезает из UI до alternate route).
  • Upgrade path: если поставщик когда-либо даёт API — scraper deprecate’ся и заменяется REST-connector’ом без изменений в core (Capabilities меняют TransportKind).

Рассмотренные альтернативы

A. Универсальный scraping фреймворк (Scrapy-like)

Избыточная обобщённость. Наши connectors детерминированы, golden fixtures конкретны. Отдельный framework layer не окупает сложности.

B. Outsource scraping внешнему сервису (Apify / ScrapingBee)

Плюс: vendor делает compliance. Минус: vendor lock-in, задержка > 1 мин, стоимость линейна к объёму запросов, privacy (их прокси видит наши credentials customer’а, если scraping требует login).

C. Только manual file-import + отказ от live scraping

Теряем актуальность цен/остатков. Смысл Tracium размывается.

Ссылки

  • ADR-0008 (rate limiter) — двухуровневый бюджет применяется и к scraper’ам.
  • ADR-0011 (no-proxy), ADR-0014 (graceful degradation) — общие invariants.
  • ADR-0024 (supplier connector contract) — interface + Capabilities расширены.
  • ADR-0026 (marketplace observations) — scraper’ы часто дают marketplace-данные.
  • ../integration-patterns.md — Pattern 4 section.
  • ../../50-processes/adding-new-supplier.md — compliance checklist добавлен.