Initial commit: Supermarket scraper MVP

This commit is contained in:
2025-12-28 23:29:30 +05:00
commit 19c0426cdc
30 changed files with 4839 additions and 0 deletions

View 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. **Масштабируемость**: Архитектура должна позволять легко добавлять новые магазины и методы скрапинга
```

View 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