Files
videoreg.ru/tasks/002-crawler.md
T
huncode 2ce58fcbc0
Master Production Deploy / production (push) Failing after 49s
feat: complete dashcam catalog ingestion demo
2026-07-31 02:01:58 +03:00

18 KiB
Raw Blame History

Спека: Yandex Market Dashcam Catalog Ingestion

1. Цель

Построить поддерживаемый ingestion engine для каталога автомобильных видеорегистраторов.

Целевая категория:

Автомобильные видеорегистраторы

Публичное приложение должно получать только approved canonical catalog:

  • название
  • бренд
  • модель
  • нормализованные характеристики
  • изображения
  • source metadata
  • дату первого обнаружения
  • дату последнего обновления

В MVP не парсим и не экспортируем:

  • цены
  • продавцов
  • наличие
  • тексты отзывов
  • авторов отзывов
  • review content

Общий aggregate rating можно сохранять внутри ingestion как source signal, но он не входит в публичный export и не участвует в stableContentHash.

Главная мысль: не "краулер, который пролезет", а каталоговый ingestion engine, который можно поддерживать годами.


2. Границы и запреты

Не реализовывать:

  • обход rate limiter
  • маскировку под реальных пользователей
  • ротацию User-Agent / browser fingerprint
  • прокси-фермы
  • headless stealth
  • обход captcha
  • агрессивную параллельную загрузку
  • регулярный browser-based crawl

В MVP fetcher работает только через обычный HTTP fetch.

Headless browser допустим только как ручной diagnostic tool для анализа структуры страницы. Он не является fallback в регулярном crawl path.


3. Источники

Primary

  • публичные category pages Яндекс Маркета
  • публичные product pages
  • JSON-LD, если доступен в HTML
  • embedded state, если доступен в HTML
  • sitemap, но только в ограниченных границах

Preferred official fallback

  • Yandex Market Partner API, если доступен по условиям проекта
  • фиды продавцов, если доступны легально
  • фиды брендов
  • официальные сайты производителей

4. Deployment Model

Ingestion деплоится в то же облако, что и web-приложение (один Swarm/Portainer environment), а не на отдельный сервер. Раньше предполагался отдельный cloud, но управлять двумя независимыми облаками оказалось слишком сложно. См. решение videoreg-nnv.3.

Код хранится в этом же репозитории:

ingestion/
  src/
  migrations/
  docker-compose.yml
  Dockerfile

Минимальные сервисы:

postgres
ingestion-worker
ingestion-admin

export-server больше не нужен: ingestion-worker пишет валидированный catalog.json + manifest.json в общий (shared) export-том, а web читает их напрямую с этого тома. Никакого HTTP-экспорта и Bearer-токена.

Основное Next.js-приложение по-прежнему не подключается напрямую к ingestion DB — единственный контакт с каталогом идёт через export-артефакт на shared-томе.


5. Source of Truth и Bridge

Source of truth для каталога: PostgreSQL ingestion stack.

Текущее примитивное хранилище приложения (data/videoreg.json) является только read-model cache.

Bridge работает со стороны приложения через общий export-том:

  1. ingestion пишет versioned export (manifest.json + catalog.json) на shared-том;
  2. приложение/сборка читает manifest.json с того же тома;
  3. проверяет schemaVersion, exportId, createdAt, sha256, itemCount;
  4. читает catalog.json с тома;
  5. сверяет sha256 артефакта;
  6. валидирует payload через shared Zod schema;
  7. атомарно заменяет data/videoreg.json (или используется как build-time источник).

В MVP import запускается вручную оператором, не по расписанию. HTTP/токен в этом обмене не участвуют — оба сервиса в одном облаке и делят том.


6. Export Contract

Export публикуется как файлы на общем (shared) export-томе: manifest.json и artifacts/<exportId>/catalog.json. HTTP-эндпоинт и Bearer-токен не используются — ingestion и web живут в одном облаке и делят том. Целостность гарантируется sha256 в манифесте и shared Zod-схемой при чтении.

Manifest

type CatalogExportManifest = {
  schemaVersion: 1;
  exportId: string;
  createdAt: string;
  artifactPath: string;
  sha256: string;
  itemCount: number;
};

Catalog Artifact

type DashcamCatalogExport = {
  schemaVersion: 1;
  exportId: string;
  createdAt: string;
  products: ExportedDashcamProduct[];
};

type ExportedDashcamProduct = {
  id: string;
  brand: string;
  model: string;
  canonicalName?: string;
  title: string;
  specs: Record<string, string | number | boolean>;
  images: ExportedProductImage[];
  sources: ExportedProductSource[];
  discoveredAt: string;
  updatedAt: string;
};

type ExportedProductImage = {
  url: string;
  originalUrl: string;
  source: MarketSource;
  sha256?: string;
  width?: number;
  height?: number;
};

type ExportedProductSource = {
  source: MarketSource;
  sourceProductId?: string;
  url: string;
  stableContentHash: string;
  fetchedAt: string;
};

type MarketSource = "yandex_market" | "mvideo";

Runtime validation: shared Zod schema в репозитории. TypeScript-типы без runtime-валидации недостаточны.


7. Image Policy

Ingestion кеширует approved images и публикует versioned image artifact URLs.

Export содержит:

  • URL versioned image artifact
  • original source URL
  • source metadata
  • optional checksum/dimensions

Приложение не должно hotlink-ить source images напрямую.

Юридическое решение о хранении и показе изображений должно быть принято отдельно до production-публикации.


8. Crawl Policy

Нагрузка:

  • 1 запрос за 5-15 секунд
  • максимум 100-300 страниц в сутки на источник
  • jitter между запросами
  • exponential backoff при 429/403/5xx
  • остановка источника при captcha
  • отдельный дневной budget на category pages, product pages, sitemap
  • не скачивать повторно страницы, если stable content не изменился

HTTP-only fetcher должен явно детектить:

rate_limited
blocked
captcha
not_found
parse_failed
needs_manual_review

При captcha, 403, 429 источник ставится на паузу. Не пытаться обходить ограничение.


9. Discovery Strategy

Discovery работает sitemap-first, но не обходит широкий sitemap всего Маркета.

Границы discovery задают:

  • ручные seed category URLs
  • ручные seed product URL patterns
  • product URLs, найденные из category HTTP discovery
  • уже известные product URLs

Sitemap используется для:

  • diff известных URL
  • refresh кандидатных URL
  • обнаружение URL рядом с уже известными границами

Category pages используются вторично:

  • первая страница категории
  • пагинация, если доступна без JS
  • top listings
  • новые product URLs, если они есть в HTML

Если category HTML бедный без JS, discovery не должен переходить в regular headless crawl. Такие случаи уходят в needs_manual_review или official fallback.

type DiscoveredProduct = {
  source: MarketSource;
  categoryUrl?: string;
  productUrl: string;
  sourceProductId?: string;
  titlePreview?: string;
  discoveredAt: string;
};

10. Fetch Queue

Очередь реализуется в PostgreSQL, без Redis/BullMQ в MVP.

State machine:

pending
 -> fetching
 -> fetched
 -> parsed
 -> normalized
 -> deduplicated
 -> needs_review
 -> approved
 -> exported

Ошибочные состояния:

rate_limited
blocked
captcha
not_found
parse_failed
needs_manual_review

Для каждой задачи хранить:

  • source
  • URL
  • job type
  • state
  • attempts
  • nextRunAt
  • lastError
  • createdAt
  • updatedAt

Backoff и daily budgets считаются в PostgreSQL.


11. Parser

Parser извлекает только разрешенные поля:

  • title
  • brand
  • model
  • specs
  • images
  • sourceProductId
  • aggregate rating как internal-only signal

Приоритет источников внутри страницы:

JSON-LD > embedded state > semantic DOM > fallback DOM

Не извлекать:

  • prices
  • sellers
  • availability
  • review texts
  • review authors
type ParsedMarketProduct = {
  source: MarketSource;
  sourceProductId?: string;
  title: string;
  brand?: string;
  model?: string;
  aggregateRating?: {
    value: number;
    scale?: number;
    ratingCount?: number;
  };
  images: string[];
  specs: Record<string, string | number | boolean>;
  sourceUrl: string;
};

12. Snapshot и Checksum Model

Сырой snapshot хранить всегда. Парсер должен уметь переобрабатывать старые страницы.

Нельзя использовать hash всего HTML как сигнал "страница изменилась": динамическая верстка, цены, рекомендации, счетчики и timestamps будут постоянно шуметь.

Для каждого snapshot хранить два хэша:

type ProductSourceSnapshot = {
  source: MarketSource;
  url: string;
  fetchedAt: string;
  httpStatus: number;
  rawSnapshotHash: string;
  stableContentHash?: string;
  rawHtmlPath?: string;
  parsedJson: Record<string, unknown>;
};

rawSnapshotHash

Хэш точного raw snapshot для аудита и debug.

stableContentHash

Source-specific semantic hash. Для Яндекс Маркета в MVP включать:

  • sourceProductId
  • canonical product URL
  • title
  • brand
  • model
  • normalized specs
  • image identifiers / original image URLs

Исключать:

  • price
  • sellers
  • availability
  • aggregate rating
  • review count
  • timestamps
  • layout
  • recommendations
  • counters
  • tracking data

13. Normalization

Нужно привести хаотичные характеристики к единому виду.

Пример:

{
  "resolution_front": "3840x2160",
  "channels": 2,
  "sensor_front": "Sony STARVIS 2",
  "gps": true,
  "wifi": true,
  "screen": true,
  "parking_mode": true,
  "memory_card_max_gb": 256,
  "view_angle_degrees": 140
}

Компоненты normalizer:

  • brand dictionary
  • model cleaner
  • spec mapper
  • unit converter
  • boolean detector
  • alias dictionary

14. Deduplication

Дедупликация работает до модерации. Единица модерации: canonical product, уже сгруппированный из вариантов и source URLs.

Сравнивать:

  • brand
  • normalized model name
  • aliases
  • sourceProductId
  • image perceptual hash
  • specs similarity
  • product URL history

Статусы:

new_product
same_product
possible_duplicate
variant
oem_clone

possible_duplicate, variant, oem_clone не должны попадать в public export без ручного решения.


15. Moderation Admin

Админка живет внутри отдельного ingestion stack.

Она работает напрямую с ingestion PostgreSQL, snapshots, очередями и canonical products.

MVP-экраны:

  • новые canonical products
  • bulk select
  • "разрешить все"
  • товары с parse_failed
  • возможные дубли
  • конфликтующие характеристики
  • история изменений
  • ручное объединение моделей
  • ручное редактирование canonical specs

Публикация в export разрешена только после ручной модерации. Bulk approve допустим, но утверждает canonical products, а не отдельные source URLs.


16. Database

Минимальная PostgreSQL-модель:

products
product_variants
product_sources
source_snapshots
product_specs
product_images
source_ratings
crawl_jobs
crawl_errors
moderation_queue
export_runs

Принципы:

  • raw HTML/JSON snapshots и image artifacts хранить на mounted volume
  • в PostgreSQL хранить пути, checksum, metadata и связи
  • уникальность по source + url
  • ingestion DB является source of truth
  • data/videoreg.json приложения является replaceable read-model cache

17. Sync Schedule

Частый легкий sync

Каждые 6-12 часов:

  • первая страница категории
  • sitemap diff в пределах известных границ
  • новые product URLs
  • изменение top listings, если доступно без JS

Глубокий sync

Раз в 7-14 дней:

  • все известные карточки
  • характеристики
  • изображения
  • source metadata

New product detection

Каждый день:

  • category diff
  • bounded sitemap diff
  • known model aliases search
  • official fallback feeds, если доступны

18. MVP Phases

  • проверить robots.txt
  • проверить sitemap
  • проверить доступность API/фидов
  • описать crawl budget
  • описать allowed URL patterns
  • зафиксировать image storage policy

Phase 2 - Shared contract

  • добавить shared Zod schema для manifest/catalog export
  • добавить TypeScript-типы из схем
  • описать schemaVersion
  • добавить app import command
  • заменить data/videoreg.json validated catalog payload

Phase 3 - Ingestion stack skeleton

  • создать ingestion/
  • добавить docker-compose.yml
  • поднять PostgreSQL
  • добавить migrations
  • добавить mounted volume для snapshots/images
  • добавить healthcheck

Phase 4 - Queue and fetcher

  • реализовать PostgreSQL-backed queue
  • реализовать HTTP-only fetcher
  • добавить rate limits, retry, backoff
  • stop on captcha / 403 / 429
  • сохранять raw snapshots
  • считать rawSnapshotHash

Phase 5 - Parser

  • category parser
  • bounded sitemap discovery
  • product parser
  • specs parser
  • image parser
  • aggregate rating parser as internal-only
  • source-specific stableContentHash

Phase 6 - Normalizer

  • brand dictionary
  • model cleaner
  • spec mapper
  • unit converter
  • boolean detector

Phase 7 - Deduplication and moderation

  • exact match
  • fuzzy match
  • alias match
  • canonical product grouping
  • moderation queue
  • bulk approve

Phase 8 - Export and import

  • publish versioned manifest
  • publish versioned catalog artifact
  • publish versioned image artifacts
  • write export artifacts to the shared export volume (no HTTP, no token)
  • verify checksum in app import
  • atomic replace of data/videoreg.json

19. Definition of Done

Система считается готовой, если:

  • ingestion и web деплоятся в одно облако; export идёт через shared-том, без HTTP/токена
  • PostgreSQL является source of truth
  • приложение импортирует только approved canonical catalog
  • data/videoreg.json заменяется только после manifest/checksum/schema validation
  • crawler не создает заметной нагрузки на источник
  • crawler останавливается при ограничениях
  • crawler не использует regular headless/browser stealth
  • raw snapshots хранятся всегда
  • есть rawSnapshotHash и source-specific stableContentHash
  • старые snapshots можно переобработать новым parser
  • дубли попадают в moderation queue
  • bulk approve работает по canonical products
  • public export не содержит цены, продавцов, наличие и тексты отзывов
  • public export не содержит aggregate rating в MVP
  • approved images отдаются как versioned image artifacts