const axios = require('axios'); const cheerio = require('cheerio'); const Product = require('../models/product'); const ProductInfo = require('../models/productInfo'); const tools = require('../tools/general'); class AmazonBookScraper { constructor() { this.baseUrl = 'https://www.amazon.it/dp/'; } async fetchPageISBN10(isbn10) { const url = `${this.baseUrl}${isbn10}`; try { const { data } = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/113.0.0.0 Safari/537.36', // altri header se necessario }, }); return data; } catch (err) { console.error(`Errore fetching ISBN ${isbn10}:`, err.message); return null; } } isbn13to10(isbn13) { try { if (!isbn13.startsWith('978') || isbn13.length !== 13) return null; const core = isbn13.slice(3, 12); // i 9 numeri centrali let sum = 0; for (let i = 0; i < 9; i++) { sum += (10 - i) * parseInt(core[i], 10); } let check = 11 - (sum % 11); if (check === 10) check = 'X'; else if (check === 11) check = '0'; else check = check.toString(); return core + check; } catch (err) { console.error('Errore calcolando ISBN-10 da ISBN-13:', err.message); return null; } } getTitleByProductInfo(productInfo) { try { return productInfo?.name.trim(); } catch (e) { return ''; } } async extractData(myproduct, html) { const $ = cheerio.load(html); const productInfo = await ProductInfo.findOne({ _id: myproduct.idProductInfo }).lean(); const title_ondb = this.getTitleByProductInfo(productInfo); // Titolo let title = $('#productTitle').text().trim() || null; // Sottotitolo (Amazon lo mette nel titolo) let dettagli = $('#productSubtitle').text().trim() || null; const ris = this.extractSubtitle(title, title_ondb); let sottotitolo = title_ondb ? ris?.subtitle : ''; let titoloOriginale = ''; if (ris?.titoloOriginale) { // E' presente il vero titolo del Libro ! titoloOriginale = ris.titoloOriginale; } if (sottotitolo && title_ondb) { // significa che il titolo è dentro al sottotitolo, quindi prendi per buono quello nostro title = title_ondb; } let numpagine = null; let misure = null; let edizione = null; let publisher = null; let data_pubblicazione = null; numpagine = this.extractNumeroDiPagine($); const dim = this.extractDimensions($); misure = this.convertDimensionsToMisureMacro(dim); const data_pubb = this.extractDataPubblicazione($); data_pubblicazione = this.parseItalianDate(data_pubb); publisher = this.extractEditore($); if (data_pubb) { edizione = this.extractMonthYear(data_pubb); } return { titolo: title, ...(titoloOriginale ? { titoloOriginale } : {}), ...(sottotitolo ? { sottotitolo } : {sottotitolo: ''}), numpagine, misure, edizione, data_pubblicazione, editore: publisher, }; } async scrapeISBN(myproduct, isbn, options) { const isbn10 = this.isbn13to10(isbn); const html = await this.fetchPageISBN10(isbn10); if (!html) return null; const data = await this.extractData(myproduct, html); if (!options?.update) return data; const arrvariazioni = myproduct.arrvariazioni || []; let index = -1; if (arrvariazioni.length === 1) { index = 0; } else { index = arrvariazioni.findIndex((v) => v.versione === shared_consts.PRODUCTTYPE.NUOVO); if (index < 0) index = 0; } const productInfo = {}; let aggiornaDataPubb = false; let aggiornaPages = false; let aggiornaMisure = false; let aggiornaEdizione = false; if (index !== -1) { const variante = arrvariazioni[index]; // Determina se aggiornare pagine aggiornaPages = !options.aggiornasoloSeVuoti || !variante.pagine || variante.pagine === 0; if (aggiornaPages) variante.pagine = Number(data.numpagine); // Determina se aggiornare misure aggiornaMisure = !options.aggiornasoloSeVuoti || !variante.misure; if (aggiornaMisure) variante.misure = data.misure; // Determina se aggiornare edizione aggiornaEdizione = !options.aggiornasoloSeVuoti || !variante.edizione; if (aggiornaEdizione) variante.edizione = data.edizione; } // Determina se aggiornare data pubblicazione const currentDatePub = productInfo.date_pub; aggiornaDataPubb = !options.aggiornasoloSeVuoti || !tools.isDateValid(currentDatePub); if (aggiornaDataPubb && data.data_pubblicazione) { productInfo.date_pub = new Date(data.data_pubblicazione); } // Aggiorna arrvariazioni se pagine o misure sono cambiati const aggiornadati = aggiornaPages || aggiornaMisure || aggiornaEdizione; if (aggiornadati) { await Product.findOneAndUpdate( { _id: myproduct._id }, { $set: { arrvariazioni, scraped: true, scraped_date: new Date() } }, { upsert: true, new: true, includeResultMetadata: true } ); } // Aggiorna productInfo se contiene dati if (!tools.isObjectEmpty(productInfo)) { const risupdate = await ProductInfo.findOneAndUpdate( { _id: myproduct.idProductInfo }, { $set: productInfo, scraped: true, scraped_date: new Date() }, { new: true, upsert: true, returnOriginal: false } ).lean(); console.log('risupdate', risupdate) ; } return data; } async scrapeMultiple(isbnList, options) { const results = []; for (const isbn of isbnList) { console.log(`Scraping ISBN: ${isbn}`); const myproduct = null; /// myproduct... const data = await this.scrapeISBN(myproduct, isbn, options); results.push({ isbn, ...data }); // Per evitare blocchi, metti una pausa (es. 2 secondi) await new Promise((r) => setTimeout(r, 2000)); } return results; } generateHtmlTableFromObject(obj) { if (!obj || typeof obj !== 'object') return ''; let html = ''; html += ''; for (const [key, value] of Object.entries(obj)) { // Se il valore è un oggetto o array, lo converto in JSON stringa const displayValue = value && typeof value === 'object' ? JSON.stringify(value) : String(value); html += ``; } html += '
ChiaveValore
${key}${displayValue}
'; return html; } async ScraperDataAmazon(idapp, options) { const scraper = new AmazonBookScraper(); const isbn = options.isbn; try { const myproduct = await Product.getProductByIsbn(isbn); const data = await scraper.scrapeISBN(myproduct, isbn, options); // let html = this.generateHtmlTableFromObject(data); console.log(data); return res.status(200).send({ code: server_constants.RIS_CODE_OK, data, html }); } catch (e) { console.error(e); return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' }); } } async ScraperMultipleDataAmazon(idapp, options) { const scraper = new AmazonBookScraper(); const isbnList = ['8850224248']; // metti i tuoi ISBN qui try { const books = await scraper.scrapeMultiple(isbnList); console.log(books); } catch (e) { console.error(e); return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' }); } } extractDataPubblicazione($) { // Seleziona il div con id specifico per la data di pubblicazione const publicationDate = $('#rpi-attribute-book_details-publication_date .rpi-attribute-value span').text().trim(); // Se non trova la data, ritorna null return publicationDate || null; } extractNumeroDiPagine($) { // Seleziona il div con id specifico per pagine const pagesText = $('#rpi-attribute-book_details-fiona_pages .rpi-attribute-value span').text().trim(); // pagesText dovrebbe essere tipo "184 pagine" if (!pagesText) return null; // Estrai solo il numero (facoltativo) const match = pagesText.match(/(\d+)/); return match ? match[1] : pagesText; // ritorna solo il numero o il testo intero } extractDimensions($) { // Seleziona il div con id specifico per dimensioni const dimText = $('#rpi-attribute-book_details-dimensions .rpi-attribute-value span').text().trim(); // Se non trova niente ritorna null return dimText || null; } convertDimensionsToMisureMacro(dimString) { if (!dimString) return null; // Estrai tutti i numeri (compresi decimali) const numbers = dimString.match(/[\d.]+/g); if (!numbers || numbers.length < 2) return null; // Converti in numeri float e ordina decrescente const sortedNums = numbers .map((num) => parseFloat(num)) .filter((n) => !isNaN(n)) .sort((a, b) => b - a); if (sortedNums.length < 2) return null; // Prendi i due più grandi const [first, second] = sortedNums; return `cm ${first}x${second}`; } parseItalianDate(dateStr) { if (!dateStr) return null; // Mappa mesi in italiano a numeri (0-based per Date) const months = { gennaio: 0, febbraio: 1, marzo: 2, aprile: 3, maggio: 4, giugno: 5, luglio: 6, agosto: 7, settembre: 8, ottobre: 9, novembre: 10, dicembre: 11, }; // Divido la stringa (es. "14 maggio 2025") const parts = dateStr.toLowerCase().split(' '); if (parts.length !== 3) return null; const day = parseInt(parts[0], 10); const month = months[parts[1]]; const year = parseInt(parts[2], 10); if (isNaN(day) || month === undefined || isNaN(year)) return null; return new Date(year, month, day); } extractSubtitle(fullTitle, baseTitle) { if (!fullTitle || !baseTitle) return null; let mybaseTitle = ''; let numCharRemoved = 0; let titoloOriginale = ''; // Se il fullTitle non contiene il baseTitle, ritorna null const coniniziali = fullTitle.trim().toLowerCase().startsWith(baseTitle.trim().toLowerCase()); if (coniniziali) { mybaseTitle = baseTitle; } let senzainiziali = false; if (!coniniziali) { // torna la posizione in cui l'ho trovato const posizione = fullTitle.toLowerCase().indexOf(baseTitle.trim().toLowerCase()); if (posizione < 0 || posizione > 3) { return null; } // torna il nome del titolo, compreso degli articoli, partendo da zero, fino a posizione + baseTitle titoloOriginale = fullTitle.substring(0, posizione + baseTitle.length); numCharRemoved = posizione; senzainiziali = true; } if (!coniniziali && !senzainiziali) { return null; } // Rimuovi il baseTitle dall'inizio let remainder = fullTitle.slice(baseTitle.length + numCharRemoved).trim(); // Se la rimanenza inizia con ":" o "-" o ".", rimuovila if (remainder.startsWith(':') || remainder.startsWith('-') || remainder.startsWith('.')) { remainder = remainder.slice(1).trim(); } // Se resta una stringa non vuota, è il sottotitolo return { subtitle: remainder.length > 0 ? remainder : null, titoloOriginale }; } extractMonthYear(dateStr) { if (!dateStr) return null; // Divide la stringa in parole const parts = dateStr.trim().split(' '); // Se ha almeno 3 parti (giorno, mese, anno) if (parts.length >= 3) { // Restituisce mese + anno return parts .slice(1) .map((part, idx) => (idx === 0 ? part[0].toUpperCase() + part.slice(1) : part)) .join(' '); } return null; } extractEditore($) { // Seleziona il testo dentro il div id rpi-attribute-book_details-publisher const publisher = $('#rpi-attribute-book_details-publisher .rpi-attribute-value span').text().trim(); return publisher || null; } } module.exports = AmazonBookScraper;