Files
videoreg.ru/tasks/002-crawler.md
T
2026-05-17 01:08:19 +03:00

651 lines
16 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 живет отдельно от облачного runtime приложения.
Код хранится в этом же репозитории:
```text
ingestion/
src/
migrations/
docker-compose.yml
Dockerfile
```
Стек ingestion деплоится отдельным `docker-compose.yml`.
Минимальные сервисы:
```text
postgres
ingestion-worker
ingestion-admin
export-server
```
Основное Next.js-приложение не подключается напрямую к ingestion DB.
---
## 5. Source of Truth и Bridge
Source of truth для каталога: PostgreSQL ingestion stack.
Текущее примитивное хранилище приложения (`data/videoreg.json`) является только read-model cache.
Bridge работает со стороны приложения:
1. ingestion публикует versioned HTTPS export;
2. приложение вручную запускает operator import command;
3. import command скачивает manifest;
4. проверяет `schemaVersion`, `exportId`, `createdAt`, `sha256`, `itemCount`;
5. скачивает catalog artifact;
6. валидирует payload через shared Zod schema;
7. атомарно заменяет `data/videoreg.json`.
В MVP import запускается вручную оператором, не по расписанию.
---
## 6. Export Contract
Export доступен по HTTPS со статичным токеном:
```http
Authorization: Bearer <token>
```
Токен передается через env import-команды. Токен не передается в query string.
### Manifest
```ts
type CatalogExportManifest = {
schemaVersion: 1;
exportId: string;
createdAt: string;
artifactUrl: 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;
title: string;
specs: Record<string, string | number | boolean>;
images: ExportedProductImage[];
sources: ExportedProductSource[];
discoveredAt: string;
updatedAt: string;
};
type ExportedProductImage = {
url: string;
originalUrl: string;
source: "yandex_market";
sha256?: string;
width?: number;
height?: number;
};
type ExportedProductSource = {
source: "yandex_market";
sourceProductId?: string;
url: string;
stableContentHash: string;
fetchedAt: string;
};
```
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: "yandex_market";
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: "yandex_market";
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: "yandex_market";
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
* protect endpoints with Bearer token
* verify checksum in app import
* atomic replace of `data/videoreg.json`
---
## 19. Definition of Done
Система считается готовой, если:
* ingestion stack запускается отдельно через `docker-compose.yml`
* 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