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 / fontrequests по умолчанию — экономит 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 добавлен.