feat: enhanced Magnit scraper with streaming mode and retry logic

- Add streaming mode for memory-efficient large catalog scraping
- Implement retry logic with exponential backoff
- Add auto session reinitialization on 403 errors
- Add configurable options (pageSize, maxProducts, rateLimitDelay)
- Add maxIterations protection against infinite loops
- Add retry.ts utility module with withRetry and withRetryAndReinit
- Update .env.example with new scraping options
- Add pgAdmin and CloudBeaver to docker-compose

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-21 22:14:04 +05:00
parent 19c0426cdc
commit 9164527f58
5 changed files with 585 additions and 74 deletions

View File

@@ -5,19 +5,50 @@ import { Logger } from '../utils/logger.js';
async function main() {
const storeCode = process.env.MAGNIT_STORE_CODE || process.argv[2];
if (!storeCode) {
Logger.error('Не указан код магазина. Используйте переменную окружения MAGNIT_STORE_CODE или передайте как аргумент');
process.exit(1);
}
// Выбор режима: streaming или legacy
const useStreaming = process.env.MAGNIT_USE_STREAMING !== 'false';
const maxProducts = process.env.MAGNIT_MAX_PRODUCTS
? parseInt(process.env.MAGNIT_MAX_PRODUCTS, 10)
: undefined;
Logger.info(`🚀 Запуск скрапинга для магазина: ${storeCode}`);
Logger.info(`Режим: ${useStreaming ? 'STREAMING' : 'LEGACY'}`);
if (maxProducts) {
Logger.info(`Лимит товаров: ${maxProducts}`);
}
const scraper = new MagnitApiScraper({
storeCode,
storeType: process.env.MAGNIT_STORE_TYPE || '6',
catalogType: process.env.MAGNIT_CATALOG_TYPE || '1',
headless: true,
headless: process.env.MAGNIT_HEADLESS !== 'false',
// Новые параметры
pageSize: process.env.MAGNIT_PAGE_SIZE ? parseInt(process.env.MAGNIT_PAGE_SIZE, 10) : 50,
maxProducts,
rateLimitDelay: process.env.MAGNIT_RATE_LIMIT_DELAY
? parseInt(process.env.MAGNIT_RATE_LIMIT_DELAY, 10)
: 300,
maxIterations: process.env.MAGNIT_MAX_ITERATIONS
? parseInt(process.env.MAGNIT_MAX_ITERATIONS, 10)
: 10000,
retryOptions: {
maxAttempts: process.env.MAGNIT_RETRY_ATTEMPTS
? parseInt(process.env.MAGNIT_RETRY_ATTEMPTS, 10)
: 3,
initialDelay: 1000,
maxDelay: 30000,
},
requestTimeout: 30000,
autoReinitOn403: true,
});
try {
@@ -27,20 +58,34 @@ async function main() {
// Инициализация скрапера
await scraper.initialize();
// Получение всех товаров
const products = await scraper.scrapeAllProducts(100);
let saved = 0;
Logger.info(`📦 Получено товаров: ${products.length}`);
if (useStreaming) {
// STREAMING режим (рекомендуется для больших каталогов)
Logger.info('📡 Использование потокового режима скрапинга');
saved = await scraper.saveToDatabaseStreaming(prisma, {
batchSize: 50,
maxProducts,
});
if (products.length > 0) {
// Сохранение в БД
const saved = await scraper.saveToDatabase(products, prisma);
Logger.info(`✅ Успешно сохранено товаров: ${saved}`);
} else {
Logger.warn('⚠️ Товары не найдены');
// LEGACY режим (обратная совместимость)
Logger.info('📦 Использование legacy режима скрапинга');
const products = await scraper.scrapeAllProducts(50);
Logger.info(`📦 Получено товаров: ${products.length}`);
if (products.length > 0) {
saved = await scraper.saveToDatabase(products, prisma);
} else {
Logger.warn('⚠️ Товары не найдены');
}
}
Logger.info(`✅ Успешно сохранено товаров: ${saved}`);
Logger.info('✅ Скрапинг завершен успешно');
} catch (error) {
Logger.error('❌ Ошибка при скрапинге:', error);
process.exit(1);