initial-commit
This commit is contained in:
205
src/scrapers/mercadolivre.js
Normal file
205
src/scrapers/mercadolivre.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* mercadolivre.js - Scraper para Mercado Livre
|
||||
*
|
||||
* Monitora a página de Ofertas do Dia do Mercado Livre
|
||||
* e extrai: nome, preço, preço original, desconto, imagem e link.
|
||||
*/
|
||||
|
||||
const { config } = require('../config');
|
||||
const { scrapeWithRetry } = require('../utils/browser');
|
||||
|
||||
/**
|
||||
* Executa o scraping da página de ofertas do Mercado Livre
|
||||
* @returns {Array<Object>} Lista de ofertas extraídas
|
||||
*/
|
||||
async function scrapeMercadoLivre() {
|
||||
console.log('🤝 [Mercado Livre] Iniciando scraping...');
|
||||
|
||||
const offers = await scrapeWithRetry(async (page) => {
|
||||
// Navegar para a página de ofertas
|
||||
await page.goto(config.urls.mercadolivre, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: config.scraping.pageTimeout,
|
||||
});
|
||||
|
||||
// Aguardar carregamento dos cards de produto
|
||||
await page.waitForSelector('.promotion-item, .poly-card, .andes-card, [class*="poly-card"], .deal-card', {
|
||||
timeout: 10000,
|
||||
}).catch(() => {
|
||||
console.log(' ⚠️ [Mercado Livre] Seletores primários não encontrados, tentando alternativas...');
|
||||
});
|
||||
|
||||
// Scroll para carregar mais itens
|
||||
await autoScroll(page);
|
||||
|
||||
// Extrair dados dos produtos
|
||||
const products = await page.evaluate((maxOffers) => {
|
||||
const items = [];
|
||||
|
||||
// Seletores para cards do Mercado Livre
|
||||
const cardSelectors = [
|
||||
'.poly-card',
|
||||
'.promotion-item',
|
||||
'.andes-card',
|
||||
'[class*="poly-card"]',
|
||||
'.deal-card',
|
||||
'.ui-search-layout__item',
|
||||
];
|
||||
|
||||
let cards = [];
|
||||
for (const selector of cardSelectors) {
|
||||
cards = document.querySelectorAll(selector);
|
||||
if (cards.length > 0) break;
|
||||
}
|
||||
|
||||
cards.forEach((card) => {
|
||||
if (items.length >= maxOffers) return;
|
||||
|
||||
try {
|
||||
// Nome do produto
|
||||
const nameEl = card.querySelector(
|
||||
'.poly-component__title, .promotion-item__title, .ui-search-item__title, [class*="title"], h2, h3'
|
||||
);
|
||||
const name = nameEl ? nameEl.textContent.trim() : '';
|
||||
|
||||
// Preço atual
|
||||
const priceEl = card.querySelector(
|
||||
'.poly-price__current .andes-money-amount, .andes-money-amount--cents-superscript, [class*="price"], .price-tag-fraction'
|
||||
);
|
||||
let price = '';
|
||||
if (priceEl) {
|
||||
// Mercado Livre separa reais e centavos em elementos diferentes
|
||||
const fractionEl = priceEl.querySelector('.andes-money-amount__fraction, .price-tag-fraction');
|
||||
const centsEl = priceEl.querySelector('.andes-money-amount__cents, .price-tag-cents');
|
||||
const currencyEl = priceEl.querySelector('.andes-money-amount__currency-symbol');
|
||||
|
||||
if (fractionEl) {
|
||||
const currency = currencyEl ? currencyEl.textContent.trim() : 'R$';
|
||||
const fraction = fractionEl.textContent.trim();
|
||||
const cents = centsEl ? centsEl.textContent.trim() : '00';
|
||||
price = `${currency} ${fraction},${cents}`;
|
||||
} else {
|
||||
price = priceEl.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Preço original
|
||||
const originalPriceEl = card.querySelector(
|
||||
'.andes-money-amount--previous, [class*="original-price"], s .andes-money-amount, .price-tag-deleted'
|
||||
);
|
||||
let originalPrice = '';
|
||||
if (originalPriceEl) {
|
||||
const origFraction = originalPriceEl.querySelector('.andes-money-amount__fraction, .price-tag-fraction');
|
||||
if (origFraction) {
|
||||
originalPrice = `R$ ${origFraction.textContent.trim()}`;
|
||||
} else {
|
||||
originalPrice = originalPriceEl.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Desconto
|
||||
const discountEl = card.querySelector(
|
||||
'.poly-component__discount, [class*="discount"], .ui-search-price__second-line__label'
|
||||
);
|
||||
const discount = discountEl ? discountEl.textContent.trim() : '';
|
||||
|
||||
// Imagem
|
||||
const imgEl = card.querySelector(
|
||||
'img[src*="http2.mlstatic"], img[data-src*="http2.mlstatic"], img[src*="meli"], img'
|
||||
);
|
||||
let image = '';
|
||||
if (imgEl) {
|
||||
image = imgEl.getAttribute('src') || imgEl.getAttribute('data-src') || '';
|
||||
// Garantir URL de imagem válida
|
||||
if (image && image.startsWith('data:')) {
|
||||
image = imgEl.getAttribute('data-src') || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Link do produto
|
||||
const linkEl = card.querySelector(
|
||||
'a[href*="mercadolivre.com.br"], a[href*="produto.mercadolivre"], a[href*="/MLB-"], a[href]'
|
||||
);
|
||||
let link = linkEl ? linkEl.getAttribute('href') : '';
|
||||
if (link && !link.startsWith('http')) {
|
||||
link = 'https://www.mercadolivre.com.br' + link;
|
||||
}
|
||||
|
||||
// Cupom (ML ocasionalmente mostra cupons)
|
||||
const couponEl = card.querySelector(
|
||||
'[class*="coupon"], [class*="cupom"], [class*="highlight"]'
|
||||
);
|
||||
let coupon = null;
|
||||
if (couponEl) {
|
||||
const couponText = couponEl.textContent.trim();
|
||||
if (couponText.toLowerCase().includes('cupom') || couponText.toLowerCase().includes('off')) {
|
||||
coupon = couponText;
|
||||
}
|
||||
}
|
||||
|
||||
// Frete grátis (informação extra útil)
|
||||
const freeShippingEl = card.querySelector(
|
||||
'[class*="free-shipping"], .poly-component__shipping, [class*="shipping"]'
|
||||
);
|
||||
const hasFreeShipping = freeShippingEl &&
|
||||
freeShippingEl.textContent.toLowerCase().includes('grátis');
|
||||
|
||||
if (name && price && link) {
|
||||
const offer = {
|
||||
name,
|
||||
price,
|
||||
originalPrice: originalPrice || null,
|
||||
discount: discount || null,
|
||||
image: image || null,
|
||||
link,
|
||||
coupon,
|
||||
store: 'mercadolivre',
|
||||
};
|
||||
|
||||
// Adicionar info de frete grátis ao cupom/observação
|
||||
if (hasFreeShipping && !coupon) {
|
||||
offer.coupon = '🚚 Frete Grátis';
|
||||
} else if (hasFreeShipping && coupon) {
|
||||
offer.coupon += ' | 🚚 Frete Grátis';
|
||||
}
|
||||
|
||||
items.push(offer);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignorar cards com erro
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
}, config.scraping.maxOffersPerSite);
|
||||
|
||||
console.log(` ✅ [Mercado Livre] ${products.length} ofertas extraídas.`);
|
||||
return products;
|
||||
|
||||
}, 'Mercado Livre');
|
||||
|
||||
return offers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll automático
|
||||
*/
|
||||
async function autoScroll(page) {
|
||||
await page.evaluate(async () => {
|
||||
await new Promise((resolve) => {
|
||||
let totalHeight = 0;
|
||||
const distance = 400;
|
||||
const timer = setInterval(() => {
|
||||
window.scrollBy(0, distance);
|
||||
totalHeight += distance;
|
||||
if (totalHeight >= 3000) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
module.exports = { scrapeMercadoLivre };
|
||||
Reference in New Issue
Block a user