Initial commit: Supermarket scraper MVP
This commit is contained in:
993
.cursor/plans/supermarket_scraper_system_1af4ed29.plan.md
Normal file
993
.cursor/plans/supermarket_scraper_system_1af4ed29.plan.md
Normal file
@@ -0,0 +1,993 @@
|
||||
---
|
||||
name: Supermarket Scraper System
|
||||
overview: Создание комплексной системы скрапинга товаров из магазинов Магнит и Пятёрочка с веб-скрапингом (Playwright), автоматизацией Android-приложений (Appium), хранением в PostgreSQL с pgvector для embeddings, интеграцией LangChain для LLM-запросов, и планировщиком для автоматического обновления данных.
|
||||
todos: []
|
||||
---
|
||||
|
||||
# План разработки системы скрапинга товаров супермаркетов
|
||||
|
||||
## Архитектура системы
|
||||
|
||||
```javascript
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Data Sources │
|
||||
├──────────┬──────────────┬───────────────────────────────────┤
|
||||
│ API │ Web Scrapers│ Android App Scrapers │
|
||||
│ Scrapers │ (Playwright) │ (Appium) │
|
||||
│ │ │ │
|
||||
│ - Magnit │ - magnit.ru │ - Магнит Android App │
|
||||
│ API │ - 5ka.ru │ - 5ka Android App │
|
||||
│ │ │ │
|
||||
│ POST │ Fallback/ │ Для доставки и │
|
||||
│ /webgate │ Дополнение │ позиций доставки │
|
||||
└────┬─────┴──────┬───────┴──────────┬────────────────────────┘
|
||||
│ │ │
|
||||
└────────────┴──────────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ Data Processing Layer │
|
||||
│ - Parsing & Normalize │
|
||||
│ - Embeddings Generation│
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ PostgreSQL Database │
|
||||
│ - pgvector extension │
|
||||
│ - Products, Categories │
|
||||
│ - Price History │
|
||||
│ - Stores │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ LangChain + LLM │
|
||||
│ - SQL Agent │
|
||||
│ - Query Interface │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```javascript
|
||||
supermarket/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── .env.example
|
||||
├── docker-compose.yml # PostgreSQL с pgvector
|
||||
├── src/
|
||||
│ ├── config/
|
||||
│ │ ├── database.ts # DB connection & setup
|
||||
│ │ ├── appium.ts # Appium config
|
||||
│ │ └── llm.ts # LangChain/LLM config
|
||||
│ │
|
||||
│ ├── database/
|
||||
│ │ ├── prisma/
|
||||
│ │ │ ├── schema.prisma # Prisma schema
|
||||
│ │ │ └── migrations/ # Автоматические миграции Prisma
|
||||
│ │ ├── client.ts # Prisma Client instance
|
||||
│ │ └── seeders/ # Initial data (Prisma seed)
|
||||
│ │
|
||||
│ ├── scrapers/
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── base/
|
||||
│ │ │ │ └── BaseApiScraper.ts
|
||||
│ │ │ ├── magnit/
|
||||
│ │ │ │ ├── MagnitApiScraper.ts
|
||||
│ │ │ │ ├── types.ts
|
||||
│ │ │ │ └── endpoints.ts # API endpoints и параметры
|
||||
│ │ │ └── 5ka/
|
||||
│ │ │ ├── 5kaApiScraper.ts
|
||||
│ │ │ ├── types.ts
|
||||
│ │ │ └── endpoints.ts
|
||||
│ │ │
|
||||
│ │ ├── web/
|
||||
│ │ │ ├── base/
|
||||
│ │ │ │ └── BaseWebScraper.ts
|
||||
│ │ │ ├── magnit/
|
||||
│ │ │ │ ├── MagnitWebScraper.ts
|
||||
│ │ │ │ ├── types.ts
|
||||
│ │ │ │ └── selectors.ts
|
||||
│ │ │ └── 5ka/
|
||||
│ │ │ ├── 5kaWebScraper.ts
|
||||
│ │ │ ├── types.ts
|
||||
│ │ │ └── selectors.ts
|
||||
│ │ │
|
||||
│ │ └── android/
|
||||
│ │ ├── base/
|
||||
│ │ │ └── BaseAppScraper.ts
|
||||
│ │ ├── magnit/
|
||||
│ │ │ ├── MagnitAppScraper.ts
|
||||
│ │ │ └── selectors.ts
|
||||
│ │ └── 5ka/
|
||||
│ │ ├── 5kaAppScraper.ts
|
||||
│ │ └── selectors.ts
|
||||
│ │
|
||||
│ ├── services/
|
||||
│ │ ├── auth/
|
||||
│ │ │ ├── WebAuthService.ts
|
||||
│ │ │ ├── AppAuthService.ts
|
||||
│ │ │ └── SessionManager.ts # Управление сессиями для API
|
||||
│ │ ├── embeddings/
|
||||
│ │ │ └── EmbeddingService.ts # Генерация embeddings для товаров
|
||||
│ │ ├── parser/
|
||||
│ │ │ └── ProductParser.ts # Парсинг и нормализация данных
|
||||
│ │ └── scheduler/
|
||||
│ │ └── SchedulerService.ts # Cron-like планировщик
|
||||
│ │
|
||||
│ ├── api/
|
||||
│ │ ├── server.ts # HTTP сервер (Express/Fastify)
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── index.ts # Главный роутер
|
||||
│ │ │ ├── scrapers.ts # Endpoints для скрапинга
|
||||
│ │ │ ├── embeddings.ts # Endpoints для embeddings
|
||||
│ │ │ ├── scheduler.ts # Endpoints для планировщика
|
||||
│ │ │ └── health.ts # Health check endpoints
|
||||
│ │ ├── controllers/
|
||||
│ │ │ ├── ScraperController.ts
|
||||
│ │ │ ├── EmbeddingController.ts
|
||||
│ │ │ └── SchedulerController.ts
|
||||
│ │ ├── middlewares/
|
||||
│ │ │ ├── auth.ts # Аутентификация (опционально)
|
||||
│ │ │ ├── errorHandler.ts
|
||||
│ │ │ └── requestLogger.ts
|
||||
│ │ └── types/
|
||||
│ │ └── requests.ts # Типы для API запросов
|
||||
│ │
|
||||
│ ├── llm/
|
||||
│ │ ├── agents/
|
||||
│ │ │ └── SQLAgent.ts # LangChain SQL Agent
|
||||
│ │ ├── chains/
|
||||
│ │ │ └── QueryChain.ts # Цепочки запросов
|
||||
│ │ └── prompts/
|
||||
│ │ └── prompts.ts # Промпты для LLM
|
||||
│ │
|
||||
│ ├── scripts/
|
||||
│ │ ├── scrape-api.ts # Ручной запуск API-скрапинга
|
||||
│ │ ├── scrape-web.ts # Ручной запуск веб-скрапинга (fallback)
|
||||
│ │ ├── scrape-android.ts # Ручной запуск Android-скрапинга
|
||||
│ │ ├── generate-embeddings.ts # Генерация embeddings
|
||||
│ │ ├── migrate.ts # Запуск Prisma миграций (prisma migrate)
|
||||
│ │ └── start-server.ts # Запуск API сервера
|
||||
│ │
|
||||
│ └── utils/
|
||||
│ ├── logger.ts
|
||||
│ └── errors.ts
|
||||
│
|
||||
└── tests/
|
||||
├── scrapers/
|
||||
└── services/
|
||||
```
|
||||
|
||||
|
||||
|
||||
## База данных (PostgreSQL + pgvector + Prisma)
|
||||
|
||||
### Prisma Schema - Основные модели:
|
||||
|
||||
```prisma
|
||||
model Store {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
type String // "web" | "app"
|
||||
code String? // storeCode для API (например "992301")
|
||||
url String?
|
||||
region String?
|
||||
address String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
products Product[]
|
||||
sessions ScrapingSession[]
|
||||
}
|
||||
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
externalId Int? // ID из внешнего API
|
||||
name String
|
||||
parentId Int?
|
||||
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id])
|
||||
children Category[] @relation("CategoryHierarchy")
|
||||
description String?
|
||||
embedding Unsupported("vector(1536)")? // pgvector (добавится в Этапе 3)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
products Product[]
|
||||
}
|
||||
|
||||
model Product {
|
||||
id Int @id @default(autoincrement())
|
||||
externalId String // ID из API (например "1000300796")
|
||||
storeId Int
|
||||
categoryId Int?
|
||||
name String
|
||||
description String?
|
||||
url String?
|
||||
imageUrl String?
|
||||
currentPrice Decimal @db.Decimal(10, 2)
|
||||
unit String? // единица измерения
|
||||
weight String? // вес/объем
|
||||
brand String?
|
||||
embedding Unsupported("vector(1536)")? // pgvector (добавится в Этапе 3)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
store Store @relation(fields: [storeId], references: [id])
|
||||
category Category? @relation(fields: [categoryId], references: [id])
|
||||
priceHistory PriceHistory[]
|
||||
|
||||
@@unique([externalId, storeId])
|
||||
@@index([storeId])
|
||||
@@index([categoryId])
|
||||
}
|
||||
|
||||
model PriceHistory {
|
||||
id Int @id @default(autoincrement())
|
||||
productId Int
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
oldPrice Decimal? @db.Decimal(10, 2) // старая цена при акции
|
||||
source String // "api" | "web" | "app"
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
|
||||
@@index([productId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model ScrapingSession {
|
||||
id Int @id @default(autoincrement())
|
||||
storeId Int
|
||||
sourceType String // "api" | "web" | "app"
|
||||
status String // "pending" | "running" | "completed" | "failed"
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
error String?
|
||||
|
||||
store Store @relation(fields: [storeId], references: [id])
|
||||
|
||||
@@index([storeId])
|
||||
@@index([status])
|
||||
@@index([startedAt])
|
||||
}
|
||||
```
|
||||
|
||||
**Примечания:**
|
||||
|
||||
- `Unsupported("vector(1536)")` - используется для pgvector, Prisma не имеет нативной поддержки
|
||||
- Для работы с pgvector используем Prisma Raw queries
|
||||
- Embeddings добавляются в Этапе 3 (после настройки базовой схемы)
|
||||
|
||||
## Этапы реализации (приоритеты разработки)
|
||||
|
||||
### Этап 1: База данных и Prisma (MVP - начало)
|
||||
|
||||
**Цель**: Настроить БД для сохранения товаров
|
||||
|
||||
1. Инициализация TypeScript проекта
|
||||
2. Настройка PostgreSQL с pgvector (Docker)
|
||||
3. Установка и настройка Prisma ORM
|
||||
4. Создание Prisma schema:
|
||||
|
||||
- Модель Store (магазины)
|
||||
- Модель Category (категории)
|
||||
- Модель Product (товары) - базовые поля без embeddings
|
||||
- Модель PriceHistory (история цен)
|
||||
- Модель ScrapingSession (сессии скрапинга)
|
||||
|
||||
5. Создание миграций Prisma
|
||||
6. Настройка Prisma Client и подключения к БД
|
||||
7. Тестирование подключения и базовых операций
|
||||
|
||||
### Этап 2: API скрапинг Магнита и сохранение в БД (MVP - продолжение)
|
||||
|
||||
**Цель**: Получить данные через API Магнита и сохранить в БД
|
||||
|
||||
1. Настройка базовых зависимостей (playwright, axios, dotenv)
|
||||
2. Реализация MagnitApiScraper с обходом защиты (Playwright для сессии)
|
||||
|
||||
- Метод инициализации сессии через Playwright
|
||||
- Метод поиска товаров (POST /webgate/v2/goods/search)
|
||||
- Получение 2-3 страниц товаров для тестирования
|
||||
- Парсинг и нормализация данных из API ответа
|
||||
|
||||
3. Интеграция MagnitApiScraper с БД через Prisma
|
||||
4. Сохранение товаров в БД через Prisma Client
|
||||
5. Сохранение истории цен при каждом скрапинге
|
||||
6. Тестирование на нескольких категориях товаров
|
||||
7. Проверка сохранения данных в PostgreSQL
|
||||
|
||||
### Этап 3: Embeddings и векторный поиск
|
||||
|
||||
**Цель**: Добавить embeddings для товаров и категорий
|
||||
|
||||
1. Установка и настройка pgvector расширения в PostgreSQL
|
||||
2. Обновление Prisma schema:
|
||||
|
||||
- Добавление поля `embedding vector(1536)` в модель Product
|
||||
- Добавление поля `embedding vector(1536)` в модель Category
|
||||
|
||||
3. Создание миграции для pgvector
|
||||
4. Интеграция с моделью для embeddings (OpenAI embeddings или Ollama)
|
||||
5. Реализация EmbeddingService:
|
||||
|
||||
- Генерация embeddings для названий и описаний товаров
|
||||
- Батчовая обработка для эффективности
|
||||
|
||||
6. Обновление индексов pgvector для быстрого поиска
|
||||
7. Функции векторного поиска в PostgreSQL через Prisma Raw queries
|
||||
|
||||
### Этап 4: API-скрапинг - полная реализация
|
||||
|
||||
1. Базовый класс BaseApiScraper с общим функционалом
|
||||
|
||||
- HTTP клиент (axios) с настройкой заголовков
|
||||
- Retry logic и обработка ошибок
|
||||
- Rate limiting
|
||||
- Интеграция с Playwright для получения сессий
|
||||
|
||||
2. MagnitApiScraper для API magnit.ru/webgate/v2/goods/search
|
||||
|
||||
- Инициализация через Playwright (получение cookies и device-id)
|
||||
- Метод `initialize()` - запуск браузера, получение сессии
|
||||
- Метод `searchGoods()` - POST запрос для поиска товаров
|
||||
- Метод `getProductStores()` - GET запрос для информации о товаре в магазине
|
||||
- Обработка 403 Forbidden (автоматическое обновление сессии)
|
||||
- Получение списка категорий
|
||||
- Пагинация товаров по категориям
|
||||
- Обработка ответа API (items, prices, promotions, ratings)
|
||||
- Маппинг API ответов в единый формат
|
||||
- Управление жизненным циклом браузера (переиспользование сессии)
|
||||
|
||||
3. 5kaApiScraper (если доступен API, аналогичная структура)
|
||||
4. Сервис парсинга и нормализации товаров
|
||||
5. Сохранение данных в БД
|
||||
|
||||
### Этап 5: Расширение функционала (после MVP)
|
||||
|
||||
**Этап 5.1: 5ka API скрапинг**
|
||||
|
||||
1. Реализация 5kaApiScraper (если доступен API)
|
||||
2. Интеграция с БД через Prisma
|
||||
|
||||
**Этап 5.2: Веб-скрапинг (Playwright) - fallback/дополнение**
|
||||
|
||||
1. Базовый класс BaseWebScraper с общим функционалом
|
||||
2. MagnitWebScraper с авторизацией (если API недоступен)
|
||||
3. 5kaWebScraper с авторизацией
|
||||
4. Интеграция как резервный метод
|
||||
|
||||
**Этап 5.3: Android-скрапинг (Appium)**
|
||||
|
||||
1. Настройка Appium и WebDriverIO
|
||||
2. Базовый класс BaseAppScraper
|
||||
3. MagnitAppScraper для Android приложения
|
||||
4. 5kaAppScraper для Android приложения
|
||||
5. Сервис авторизации в приложениях
|
||||
|
||||
### Этап 6: LangChain интеграция
|
||||
|
||||
1. Настройка LangChain SQL Agent
|
||||
2. Подключение к PostgreSQL через Prisma
|
||||
3. Промпты для работы с товарами и ценами
|
||||
4. Реализация запросов типа "самый дешевый попкорн"
|
||||
|
||||
### Этап 7: Планировщик и автоматизация
|
||||
|
||||
1. SchedulerService с cron-like функционалом
|
||||
|
||||
- node-cron для расписаний
|
||||
- Управление задачами (запуск, остановка, статус)
|
||||
- Конфигурация расписаний через конфиг-файл
|
||||
|
||||
2. REST API Server для внешних интеграций
|
||||
|
||||
- HTTP сервер (Express или Fastify)
|
||||
- API endpoints для триггеров из n8n и других сервисов
|
||||
- Документация API (OpenAPI/Swagger)
|
||||
|
||||
3. API Endpoints:
|
||||
|
||||
- `POST /api/scrapers/api/magnit` - Запуск API-скрапинга Магнита
|
||||
- `POST /api/scrapers/api/5ka` - Запуск API-скрапинга 5ka
|
||||
- `POST /api/scrapers/web/magnit` - Запуск веб-скрапинга Магнита
|
||||
- `POST /api/scrapers/web/5ka` - Запуск веб-скрапинга 5ka
|
||||
- `POST /api/scrapers/android/magnit` - Запуск Android-скрапинга Магнита
|
||||
- `POST /api/scrapers/android/5ka` - Запуск Android-скрапинга 5ka
|
||||
- `POST /api/embeddings/generate` - Генерация embeddings для товаров
|
||||
- `POST /api/scheduler/jobs` - Создание задачи в планировщике
|
||||
- `GET /api/scheduler/jobs` - Список задач планировщика
|
||||
- `GET /api/scheduler/jobs/:id` - Статус конкретной задачи
|
||||
- `DELETE /api/scheduler/jobs/:id` - Удаление задачи
|
||||
- `POST /api/scheduler/jobs/:id/trigger` - Ручной запуск задачи
|
||||
- `GET /api/health` - Health check
|
||||
- `GET /api/stats` - Статистика скрапинга
|
||||
|
||||
4. Конфигурация расписаний обновления
|
||||
5. Логирование и обработка ошибок
|
||||
6. CLI для ручного запуска скриптов (как альтернатива API)
|
||||
|
||||
### Этап 8: Обработка истории цен
|
||||
|
||||
1. Автоматическое создание записей в price_history (уже в Этапе 2)
|
||||
2. Анализ изменений цен
|
||||
3. Уведомления о значительных изменениях (опционально)
|
||||
|
||||
## Технологический стек
|
||||
|
||||
- **Runtime**: Node.js + TypeScript
|
||||
- **API Scraping**: Axios/Fetch для HTTP запросов (приоритетный метод)
|
||||
- **Web Scraping**: Playwright (fallback метод)
|
||||
- **Mobile Automation**: Appium + WebDriverIO
|
||||
- **Database**: PostgreSQL 15+ с расширением pgvector
|
||||
- **ORM**: Prisma
|
||||
- **LLM Integration**: LangChain + OpenAI/Ollama
|
||||
- **Scheduler**: node-cron
|
||||
- **HTTP Server**: Express или Fastify для REST API
|
||||
- **API Documentation**: OpenAPI/Swagger
|
||||
- **Docker**: для PostgreSQL с pgvector
|
||||
|
||||
## Ключевые зависимости
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.6.2",
|
||||
"playwright": "^1.40.0",
|
||||
"webdriverio": "^8.25.0",
|
||||
"@wdio/appium-service": "^8.25.0",
|
||||
"pg": "^8.11.0",
|
||||
"pgvector": "^0.1.8",
|
||||
"typeorm": "^0.3.17",
|
||||
"langchain": "^0.1.0",
|
||||
"@langchain/openai": "^0.0.5",
|
||||
"langchain-sql": "^0.1.0",
|
||||
"openai": "^4.20.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"helmet": "^7.1.0",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"swagger-jsdoc": "^6.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/swagger-ui-express": "^4.1.6",
|
||||
"@types/swagger-jsdoc": "^6.0.4",
|
||||
"typescript": "^5.3.0",
|
||||
"ts-node": "^10.9.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## API Магнита - Структура данных и обход защиты
|
||||
|
||||
### Основной Endpoint: `POST https://magnit.ru/webgate/v2/goods/search`
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
sort: { order: "desc" | "asc", type: "popularity" | "price" | "name" },
|
||||
pagination: { limit: number, offset: number },
|
||||
categories: number[], // ID категорий
|
||||
includeAdultGoods: boolean,
|
||||
storeCode: string, // Код магазина (например "992301")
|
||||
storeType: string, // Тип магазина (например "6")
|
||||
catalogType: string // Тип каталога (например "1")
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Дополнительный Endpoint: `GET https://magnit.ru/webgate/v2/goods/{productId}/stores/{storeCode}`
|
||||
|
||||
Используется для получения информации о товаре в конкретном магазине:
|
||||
|
||||
- **URL**: `/webgate/v2/goods/1000257489/stores/992301?storetype=6&catalogtype=1`
|
||||
- **Метод**: GET
|
||||
- **Проблема**: Возвращает 403 Forbidden без правильных заголовков и cookies
|
||||
|
||||
**Response Structure (search endpoint):**
|
||||
|
||||
```typescript
|
||||
{
|
||||
category: { id: number, title: string, fullMatch: boolean },
|
||||
items: Array<{
|
||||
id: string,
|
||||
productId: string,
|
||||
name: string,
|
||||
price: number, // Цена в копейках (24999 = 249.99 руб)
|
||||
promotion?: {
|
||||
isPromotion: boolean,
|
||||
oldPrice?: number,
|
||||
discountPercent?: number,
|
||||
endDate: string
|
||||
},
|
||||
gallery: Array<{ url: string, type: string }>,
|
||||
ratings: {
|
||||
rating: number,
|
||||
scoresCount: number,
|
||||
commentsCount: number
|
||||
},
|
||||
quantity: number, // Остаток на складе
|
||||
badges: Array<{ text: string, backgroundColor: string }>,
|
||||
storeCode: string,
|
||||
// ... другие поля
|
||||
}>,
|
||||
fastCategoriesExtended: Array<{ id: number, title: string }>
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Проблема 403 Forbidden и обход защиты
|
||||
|
||||
API Магнита защищен и требует:
|
||||
|
||||
1. **Обязательные заголовки:**
|
||||
|
||||
- `x-device-id` - Device ID из cookie `mg_udi` (формат UUID: `9D4909B8-9F79-5B1D-8457-EA519EC9AA3F`)
|
||||
- `x-client-name: magnit`
|
||||
- `x-device-platform: Web`
|
||||
- `x-app-version: 2025.12.12-16.56` (актуальная версия)
|
||||
- `x-new-magnit: true`
|
||||
- `x-platform-version: Windows Chrome 130`
|
||||
- `referer: https://magnit.ru/`
|
||||
- `user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...`
|
||||
|
||||
2. **Обязательные cookies:**
|
||||
|
||||
- `shopCode=992301`
|
||||
- `mg_udi` - Device ID (UUID формат)
|
||||
- `oxxfgh`, `uwyii`, `uwyiert` - сессионные cookies
|
||||
- `nmg_dt`, `nmg_sp`, `mg_uac`, `mg_adlt` - другие служебные cookies
|
||||
|
||||
3. **Заголовки браузера:**
|
||||
|
||||
- `accept: */*`
|
||||
- `accept-language: ru,en-US;q=0.9,en;q=0.8`
|
||||
- `accept-encoding: gzip, deflate, br, zstd`
|
||||
- `sec-ch-ua`, `sec-fetch-dest`, `sec-fetch-mode`, `sec-fetch-site`
|
||||
|
||||
### Решение: Гибридный подход (Playwright + Axios)
|
||||
|
||||
**Вариант 1: Playwright для инициализации сессии, затем Axios для запросов**
|
||||
|
||||
```typescript
|
||||
// Псевдокод реализации
|
||||
class MagnitApiScraper {
|
||||
// 1. Инициализация через Playwright
|
||||
async initialize() {
|
||||
// Запуск браузера
|
||||
browser = await chromium.launch({ headless: true });
|
||||
page = await browser.newPage();
|
||||
|
||||
// Переход на главную для получения cookies
|
||||
await page.goto('https://magnit.ru/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Извлечение cookies
|
||||
cookies = await page.context().cookies();
|
||||
deviceId = cookies.find(c => c.name === 'mg_udi')?.value;
|
||||
|
||||
// Настройка axios с cookies и заголовками
|
||||
httpClient.defaults.headers.cookie = formatCookies(cookies);
|
||||
httpClient.defaults.headers['x-device-id'] = deviceId;
|
||||
// ... остальные заголовки
|
||||
}
|
||||
|
||||
// 2. API запросы через axios
|
||||
async getProductStores(productId: string, storeCode: string) {
|
||||
// Автоматическое обновление сессии при 403
|
||||
if (response.status === 403) {
|
||||
await this.initialize();
|
||||
return this.getProductStores(productId, storeCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Вариант 2: Использование Playwright Request API (рекомендуется)**
|
||||
|
||||
```typescript
|
||||
// Использование встроенного request API Playwright
|
||||
async getProductStores(productId: string, storeCode: string) {
|
||||
// Playwright автоматически передает cookies и контекст браузера
|
||||
const response = await page.request.get(
|
||||
`https://magnit.ru/webgate/v2/goods/${productId}/stores/${storeCode}`,
|
||||
{
|
||||
params: { storetype: '6', catalogtype: '1' },
|
||||
headers: {
|
||||
'accept': '*/*',
|
||||
'referer': 'https://magnit.ru/',
|
||||
'x-app-version': '2025.12.12-16.56',
|
||||
'x-client-name': 'magnit',
|
||||
'x-device-platform': 'Web',
|
||||
'x-new-magnit': 'true',
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Структура реализации в проекте
|
||||
|
||||
```typescript
|
||||
// src/scrapers/api/magnit/MagnitApiScraper.ts
|
||||
export class MagnitApiScraper extends BaseApiScraper {
|
||||
private browser: Browser | null = null;
|
||||
private page: Page | null = null;
|
||||
private deviceId: string = '';
|
||||
|
||||
async initialize() {
|
||||
// Запуск Playwright, получение cookies и device-id
|
||||
// Сохранение сессии для переиспользования
|
||||
}
|
||||
|
||||
async searchGoods(params: SearchParams) {
|
||||
// POST /webgate/v2/goods/search
|
||||
// Использование page.request.post() или axios с cookies
|
||||
}
|
||||
|
||||
async getProductStores(productId: string, storeCode: string) {
|
||||
// GET /webgate/v2/goods/{productId}/stores/{storeCode}
|
||||
// Обработка 403, автоматическое обновление сессии
|
||||
}
|
||||
|
||||
async close() {
|
||||
// Закрытие браузера
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Важные моменты реализации
|
||||
|
||||
- **Инициализация сессии**: Один раз при старте скрапера, переиспользование cookies
|
||||
- **Обновление сессии**: Автоматическое обновление при получении 403
|
||||
- **Управление браузером**: Переиспользование одного браузера для всех запросов
|
||||
- **Rate limiting**: Добавление задержек между запросами (200-500ms)
|
||||
- **Обработка ошибок**: Retry logic с экспоненциальной задержкой
|
||||
- **Кэширование**: Сохранение device-id и cookies в файл для переиспользования между запусками
|
||||
|
||||
**Важные моменты:**
|
||||
|
||||
- Цены в API хранятся в копейках (24999 = 249.99 руб)
|
||||
- Нужно получать список категорий отдельным запросом или парсить с сайта
|
||||
- Пагинация через `offset` и `limit`
|
||||
- Для каждого магазина нужен свой `storeCode`
|
||||
- **Обязательно использовать Playwright для получения cookies перед API запросами**
|
||||
|
||||
## REST API Endpoints для внешних интеграций
|
||||
|
||||
### База URL: `http://localhost:3000/api` (или переменная окружения `API_PORT`)
|
||||
|
||||
### Endpoints для скрапинга
|
||||
|
||||
#### `POST /api/scrapers/api/magnit`
|
||||
|
||||
Запуск API-скрапинга Магнита**Request Body:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
storeCode?: string; // Код магазина (по умолчанию из конфига)
|
||||
categories?: number[]; // ID категорий для скрапинга
|
||||
pagination?: { // Параметры пагинации
|
||||
limit: number;
|
||||
offset: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
jobId: string; // ID задачи
|
||||
status: "started" | "running" | "completed" | "failed";
|
||||
startedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### `POST /api/scrapers/api/5ka`
|
||||
|
||||
Запуск API-скрапинга 5ka (аналогичная структура)
|
||||
|
||||
#### `POST /api/scrapers/web/magnit`
|
||||
|
||||
Запуск веб-скрапинга Магнита через Playwright
|
||||
|
||||
#### `POST /api/scrapers/web/5ka`
|
||||
|
||||
Запуск веб-скрапинга 5ka через Playwright
|
||||
|
||||
#### `POST /api/scrapers/android/magnit`
|
||||
|
||||
Запуск Android-скрапинга Магнита через Appium
|
||||
|
||||
#### `POST /api/scrapers/android/5ka`
|
||||
|
||||
Запуск Android-скрапинга 5ka через Appium
|
||||
|
||||
### Endpoints для embeddings
|
||||
|
||||
#### `POST /api/embeddings/generate`
|
||||
|
||||
Генерация embeddings для товаров**Request Body:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
productIds?: number[]; // Конкретные товары (если не указано - все)
|
||||
batchSize?: number; // Размер батча (по умолчанию 100)
|
||||
force?: boolean; // Перегенерировать существующие
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
jobId: string;
|
||||
totalProducts: number;
|
||||
processed: number;
|
||||
status: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Endpoints для планировщика
|
||||
|
||||
#### `POST /api/scheduler/jobs`
|
||||
|
||||
Создание задачи в планировщике**Request Body:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: string; // Название задачи
|
||||
schedule: string; // Cron выражение (например "0 0 * * *")
|
||||
taskType: "api_scrape" | "web_scrape" | "android_scrape" | "generate_embeddings";
|
||||
taskConfig: { // Конфигурация задачи
|
||||
store: "magnit" | "5ka";
|
||||
categories?: number[];
|
||||
// ... другие параметры
|
||||
};
|
||||
enabled?: boolean; // Включена ли задача (по умолчанию true)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### `GET /api/scheduler/jobs`
|
||||
|
||||
Список всех задач планировщика**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
jobs: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
schedule: string;
|
||||
taskType: string;
|
||||
enabled: boolean;
|
||||
lastRun?: string;
|
||||
nextRun?: string;
|
||||
status: "idle" | "running" | "error";
|
||||
}>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### `GET /api/scheduler/jobs/:id`
|
||||
|
||||
Получить информацию о конкретной задаче
|
||||
|
||||
#### `PUT /api/scheduler/jobs/:id`
|
||||
|
||||
Обновить задачу (можно изменить schedule, enabled и т.д.)
|
||||
|
||||
#### `DELETE /api/scheduler/jobs/:id`
|
||||
|
||||
Удалить задачу из планировщика
|
||||
|
||||
#### `POST /api/scheduler/jobs/:id/trigger`
|
||||
|
||||
Ручной запуск задачи немедленно (вне расписания)**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
jobId: string;
|
||||
status: "triggered";
|
||||
startedAt: string;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Утилитарные endpoints
|
||||
|
||||
#### `GET /api/health`
|
||||
|
||||
Health check endpoint**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
status: "ok" | "error";
|
||||
timestamp: string;
|
||||
database: "connected" | "disconnected";
|
||||
services: {
|
||||
scrapers: "available" | "unavailable";
|
||||
embeddings: "available" | "unavailable";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### `GET /api/stats`
|
||||
|
||||
Статистика скрапинга**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
totalProducts: number;
|
||||
totalCategories: number;
|
||||
lastScrape: {
|
||||
magnit?: string;
|
||||
"5ka"?: string;
|
||||
};
|
||||
priceHistory: {
|
||||
totalRecords: number;
|
||||
lastUpdate: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### `GET /api/jobs/:jobId`
|
||||
|
||||
Статус конкретной задачи**Response:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
jobId: string;
|
||||
status: "pending" | "running" | "completed" | "failed";
|
||||
startedAt: string;
|
||||
finishedAt?: string;
|
||||
progress?: {
|
||||
total: number;
|
||||
processed: number;
|
||||
percentage: number;
|
||||
};
|
||||
error?: string;
|
||||
result?: any;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Интеграция с n8n
|
||||
|
||||
Для интеграции с n8n и другими автоматизационными сервисами:
|
||||
|
||||
1. **HTTP Request Node** в n8n:
|
||||
|
||||
- URL: `http://your-server:3000/api/scrapers/api/magnit`
|
||||
- Method: POST
|
||||
- Body: JSON с параметрами
|
||||
|
||||
2. **Webhook для обратных вызовов** (опционально):
|
||||
|
||||
- `POST /api/webhooks/:webhookId` - регистрация webhook
|
||||
- При завершении задачи отправка POST запроса на указанный URL
|
||||
|
||||
3. **Polling статуса**:
|
||||
|
||||
- Использовать `GET /api/jobs/:jobId` для проверки статуса
|
||||
- Или настроить webhook для уведомлений
|
||||
|
||||
### Аутентификация (опционально)
|
||||
|
||||
Для защиты API можно добавить:
|
||||
|
||||
- API Key authentication через заголовок `X-API-Key`
|
||||
- JWT токены
|
||||
- Базовая аутентификация
|
||||
```typescript
|
||||
// Пример middleware для API ключа
|
||||
app.use('/api', (req, res, next) => {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
if (apiKey === process.env.API_KEY) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## Важные моменты
|
||||
|
||||
1. **Приоритет API**: Использовать API как основной метод получения данных для Магнита
|
||||
2. **Авторизация и обход защиты**:
|
||||
|
||||
- **Для API Магнита**: Обязательно использовать Playwright для получения cookies и device-id перед API запросами
|
||||
- Инициализация сессии через браузер (один раз при старте)
|
||||
- Переиспользование cookies и device-id для всех запросов
|
||||
- Автоматическое обновление сессии при получении 403 Forbidden
|
||||
- Использование `page.request` API Playwright (рекомендуется) или передача cookies в axios
|
||||
- **Для веба**: Сохранение cookies/session для Playwright
|
||||
- **Для приложений**: Автоматизация входа через Appium
|
||||
|
||||
3. **Обработка ошибок**: Robust error handling для сетевых проблем, изменения API и структуры сайтов
|
||||
4. **Rate Limiting**: Ограничение частоты запросов чтобы не блокировали (задержки между запросами)
|
||||
5. **Мониторинг**: Логирование всех операций скрапинга (API запросы, ответы, ошибки)
|
||||
6. **Масштабируемость**: Архитектура должна позволять легко добавлять новые магазины и методы скрапинга
|
||||
7. **Единый формат данных**: Нормализация данных из разных источников (API/Web/App) в единую структуру
|
||||
8. **REST API для интеграций**: Все операции доступны через REST API для интеграции с n8n и другими сервисами
|
||||
9. **Асинхронная обработка**: Задачи скрапинга выполняются асинхронно, возвращается jobId для отслеживания статуса
|
||||
10. **Мониторинг задач**: Возможность отслеживать прогресс выполнения задач через API endpoints
|
||||
|
||||
// Пример middleware для API ключаapp.use('/api', (req, res, next) => {const apiKey = req.headers['x-api-key'];if (apiKey === process.env.API_KEY) {next();} else {res.status(401).json({ error: 'Unauthorized' });}});
|
||||
|
||||
```javascript
|
||||
|
||||
## Порядок разработки (Roadmap)
|
||||
|
||||
### Фаза 1: Минимальный прототип (MVP)
|
||||
|
||||
1. ✅ Этап 1: База данных с Prisma - настройка БД
|
||||
2. ✅ Этап 2: API скрапинг Магнита - получение данных и сохранение в БД
|
||||
3. ✅ Этап 3: Embeddings - семантический поиск
|
||||
|
||||
### Фаза 2: Расширение функционала
|
||||
|
||||
4. ⏳ Этап 4: Полная реализация API скрапинга
|
||||
5. ⏳ Этап 5: Веб и Android скрапинг (опционально)
|
||||
6. ⏳ Этап 6: LangChain интеграция
|
||||
7. ⏳ Этап 7: Планировщик и REST API
|
||||
8. ⏳ Этап 8: Аналитика цен
|
||||
|
||||
**Текущий фокус**: Этап 1 (БД) → Этап 2 (API + БД) → Этап 3 (Embeddings)
|
||||
|
||||
## Важные моменты
|
||||
|
||||
1. **Приоритет разработки**: Сначала настройка БД (Этап 1), потом API скрапинг с сохранением в БД (Этап 2), затем embeddings (Этап 3). Веб/Android скрапинг - позже
|
||||
2. **ORM выбор**: Используется Prisma (не TypeORM)
|
||||
3. **Приоритет API**: Использовать API как основной метод получения данных для Магнита
|
||||
4. **Авторизация и обход защиты**:
|
||||
|
||||
- **Для API Магнита**: Обязательно использовать Playwright для получения cookies и device-id перед API запросами
|
||||
- Инициализация сессии через браузер (один раз при старте)
|
||||
- Переиспользование cookies и device-id для всех запросов
|
||||
- Автоматическое обновление сессии при получении 403 Forbidden
|
||||
- Использование `page.request` API Playwright (рекомендуется) или передача cookies в axios
|
||||
- **Для веба**: Сохранение cookies/session для Playwright
|
||||
- **Для приложений**: Автоматизация входа через Appium
|
||||
|
||||
3. **Обработка ошибок**: Robust error handling для сетевых проблем, изменения API и структуры сайтов
|
||||
4. **Rate Limiting**: Ограничение частоты запросов чтобы не блокировали (задержки между запросами)
|
||||
5. **Мониторинг**: Логирование всех операций скрапинга (API запросы, ответы, ошибки)
|
||||
6. **Масштабируемость**: Архитектура должна позволять легко добавлять новые магазины и методы скрапинга
|
||||
7. **Единый формат данных**: Нормализация данных из разных источников (API/Web/App) в единую структуру
|
||||
8. **Масштабируемость**: Архитектура должна позволять легко добавлять новые магазины и методы скрапинга
|
||||
|
||||
|
||||
```
|
||||
173
.cursor/rules/requestly-test-rules.mdc
Normal file
173
.cursor/rules/requestly-test-rules.mdc
Normal file
@@ -0,0 +1,173 @@
|
||||
---
|
||||
globs: .requestly-supermarket/**/*.json
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
This rule provides instructions to generate API tests for Requestly requests:
|
||||
|
||||
- The Requestly HTTP requests are stored as JSON files in `.requestly-supermarket/` directory and subdirectories.
|
||||
- The `name` key shows the name of the request.
|
||||
- The `request.url` key shows the URL of the request.
|
||||
- The `request.body` key shows the body of the request. It is a string representation of a JSON body.
|
||||
- The `request.scripts.postResponse` key holds the API test. It is a string representation of a JavaScript code.
|
||||
|
||||
## Test Format
|
||||
|
||||
The test format is:
|
||||
|
||||
```js
|
||||
rq.test("Test name", () => {
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Requestly Specific Methods
|
||||
|
||||
The tests use Chai.js syntax with Requestly abstraction on top. Requestly provides a few methods on top of Chai.js.
|
||||
|
||||
### Assertion
|
||||
|
||||
Use `rq.expect` for assertions:
|
||||
|
||||
```js
|
||||
rq.expect(actualValue).to.equal(expectedValue);
|
||||
```
|
||||
|
||||
### Accessing Request and Response Objects
|
||||
|
||||
Use `rq.request` and `rq.response` to access the request and response objects. Use `rq.response.body` to access the response body. Convert it to JSON before working with it.
|
||||
|
||||
### Checking Statuses
|
||||
|
||||
Requestly provides custom status code names for some of the most common status codes. You can use these to check the response status:
|
||||
|
||||
```js
|
||||
// Success responses
|
||||
rq.response.to.be.ok // 2XX
|
||||
rq.response.to.be.success // 200
|
||||
rq.response.to.be.accepted // 202
|
||||
|
||||
// Client error responses
|
||||
rq.response.to.be.badRequest // 400
|
||||
rq.response.to.be.unauthorized // 401
|
||||
rq.response.to.be.forbidden // 403
|
||||
rq.response.to.be.notFound // 404
|
||||
rq.response.to.be.rateLimited // 429
|
||||
rq.response.to.be.clientError // 4XX
|
||||
|
||||
// Server error responses
|
||||
rq.response.to.be.serverError // 5XX
|
||||
|
||||
// Other status categories
|
||||
rq.response.to.be.error // 4XX or 5XX
|
||||
rq.response.to.be.info // 1XX
|
||||
rq.response.to.be.redirection // 3XX
|
||||
```
|
||||
|
||||
### Checking Response Body
|
||||
|
||||
Use `.to.have.body` to check the response body verbatim:
|
||||
|
||||
```js
|
||||
// Checks if response body exactly matches expected value.
|
||||
rq.response.to.have.body(expectedValue: string)
|
||||
```
|
||||
|
||||
Use `.to.have.jsonBody` to check for the existence of a particular JSON key and its value:
|
||||
|
||||
```js
|
||||
// Checks if a path exists in the response.
|
||||
rq.response.to.have.jsonBody(path: string)
|
||||
rq.response.to.have.jsonBody("user.name");
|
||||
|
||||
// Checks if a JSON path has a specific value.
|
||||
rq.response.to.have.jsonBody(path: string, value: any)
|
||||
rq.response.to.have.jsonBody("user.name", "John");
|
||||
```
|
||||
|
||||
Use `.to.have.jsonSchema` to validate against a JSON schema:
|
||||
|
||||
```js
|
||||
// Validates the response body against a JSON schema. Accepts optional Ajv configuration.
|
||||
rq.response.to.have.jsonSchema(schema: object, ajvOptions?: AjvOptions)
|
||||
|
||||
rq.response.to.have.jsonSchema({
|
||||
type: "object",
|
||||
required: ["id", "name"],
|
||||
properties: {
|
||||
id: { type: "number" },
|
||||
name: { type: "string" },
|
||||
email: { type: "string", format: "email" }
|
||||
}
|
||||
}, { allErrors: true });
|
||||
```
|
||||
|
||||
### Environment and Global Variables
|
||||
|
||||
Use `rq.environment.get("variable_name")` to get the value of an environment variable. Use `rq.globals.get("variable_name")` to get the value of a global variable. The `.requestly-supermarket/environments/global.json` has a list of all global variables. The other JSON files in the folder hold the different environments. Each key in the `variables` property is the name of the variable.
|
||||
|
||||
## Test Categories
|
||||
|
||||
The most common tests are:
|
||||
|
||||
- Response status check
|
||||
- Response body check
|
||||
- Data validation
|
||||
|
||||
## Common Test Patterns
|
||||
|
||||
### Response Status Check
|
||||
|
||||
```js
|
||||
rq.test("Request is successful", () => {
|
||||
rq.response.to.be.ok
|
||||
})
|
||||
```
|
||||
|
||||
### Response Body Check
|
||||
|
||||
```js
|
||||
// Check individual properties
|
||||
|
||||
rq.test("Response has property_name",()=>{
|
||||
var body = JSON.parse(rq.response.body)
|
||||
rq.expect(body).to.have.property("property_name").that.is.a("type")
|
||||
})
|
||||
|
||||
// Validate against JSON schema
|
||||
rq.test("Match JSON schema", () => {
|
||||
rq.response.to.have.jsonSchema({
|
||||
...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Data Validation
|
||||
|
||||
```js
|
||||
rq.test("Returns valid data", () => {
|
||||
rq.expect(body.property_name).to.equal("expected_value")
|
||||
})
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Group related tests together
|
||||
- Use descriptive test names
|
||||
- Test one concept per test
|
||||
- Include both positive and negative cases
|
||||
- First, check response status, then check the body, then check data validation
|
||||
- Test for required fields
|
||||
- Validate data types
|
||||
- Check array contents
|
||||
- Verify nested structures
|
||||
|
||||
## Specific to This Project
|
||||
|
||||
This project contains API requests for:
|
||||
- Magnit API endpoints (Russian supermarket chain)
|
||||
- The API uses POST requests with JSON bodies
|
||||
- Responses typically include arrays of items with fields like `id`, `name`, `price`, etc.
|
||||
- Prices are stored in kopecks (e.g., 24999 = 249.99 rubles)
|
||||
- Responses may include pagination information
|
||||
- Some endpoints require authentication via headers or cookies
|
||||
Reference in New Issue
Block a user