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

657 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Спека: 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`.
Код хранится в этом же репозитории:
```text
ingestion/
src/
migrations/
docker-compose.yml
Dockerfile
```
Минимальные сервисы:
```text
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
```ts
type CatalogExportManifest = {
schemaVersion: 1;
exportId: string;
createdAt: string;
artifactPath: string;
sha256: string;
itemCount: number;
};
```
### Catalog Artifact
```ts
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 должен явно детектить:
```text
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.
```ts
type DiscoveredProduct = {
source: MarketSource;
categoryUrl?: string;
productUrl: string;
sourceProductId?: string;
titlePreview?: string;
discoveredAt: string;
};
```
---
## 10. Fetch Queue
Очередь реализуется в PostgreSQL, без Redis/BullMQ в MVP.
State machine:
```text
pending
-> fetching
-> fetched
-> parsed
-> normalized
-> deduplicated
-> needs_review
-> approved
-> exported
```
Ошибочные состояния:
```text
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
Приоритет источников внутри страницы:
```text
JSON-LD > embedded state > semantic DOM > fallback DOM
```
Не извлекать:
* prices
* sellers
* availability
* review texts
* review authors
```ts
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 хранить два хэша:
```ts
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
Нужно привести хаотичные характеристики к единому виду.
Пример:
```json
{
"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
Статусы:
```text
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-модель:
```sql
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
### Phase 1 - Legal discovery
* проверить 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