- estrazione dei dati del libro sul sito di Amazon.
- possibilità di visualizzare i dati estratti e di aggiornare i dati, sia solo se vuoti, che sovrascrivere tutti i dati.
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import axios from 'axios';
|
||||
import cheerio from 'cheerio';
|
||||
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 fetchPage(isbn) {
|
||||
const url = `${this.baseUrl}${isbn}`;
|
||||
async fetchPageISBN10(isbn10) {
|
||||
const url = `${this.baseUrl}${isbn10}`;
|
||||
try {
|
||||
const { data } = await axios.get(url, {
|
||||
headers: {
|
||||
@@ -20,106 +25,380 @@ class AmazonBookScraper {
|
||||
});
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error(`Errore fetching ISBN ${isbn}:`, err.message);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
extractData(html) {
|
||||
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)
|
||||
|
||||
// Sottotitolo (Amazon spesso lo mette in #productSubtitle o nel titolo, proveremo)
|
||||
let subtitle = $('#productSubtitle').text().trim() || null;
|
||||
let dettagli = $('#productSubtitle').text().trim() || null;
|
||||
|
||||
// Numero pagine, formato, edizione
|
||||
// Questi dati spesso sono nella tabella dettagli prodotto con id #detailBullets_feature_div o #productDetailsTable
|
||||
// Proviamo a estrarre da #detailBullets_feature_div
|
||||
const ris = this.extractSubtitle(title, title_ondb);
|
||||
|
||||
let pages = null;
|
||||
let format = null;
|
||||
let edition = null;
|
||||
let sottotitolo = title_ondb ? ris?.subtitle : '';
|
||||
|
||||
$('#detailBullets_feature_div li').each((i, el) => {
|
||||
const label = $(el).find('span.a-text-bold').text().trim().toLowerCase();
|
||||
const value = $(el).find('span').last().text().trim();
|
||||
let titoloOriginale = '';
|
||||
|
||||
if (label.includes('pagine') || label.includes('pagine stampate')) {
|
||||
pages = value;
|
||||
} else if (label.includes('formato')) {
|
||||
format = value;
|
||||
} else if (label.includes('edizione')) {
|
||||
edition = value;
|
||||
}
|
||||
});
|
||||
|
||||
// fallback su #productDetailsTable (altro possibile layout)
|
||||
if (!pages || !format || !edition) {
|
||||
$('#productDetailsTable .content tr').each((i, el) => {
|
||||
const label = $(el).find('th').text().trim().toLowerCase();
|
||||
const value = $(el).find('td').text().trim();
|
||||
|
||||
if (!pages && (label.includes('pagine') || label.includes('pagine stampate'))) {
|
||||
pages = value;
|
||||
} else if (!format && label.includes('formato')) {
|
||||
format = value;
|
||||
} else if (!edition && label.includes('edizione')) {
|
||||
edition = value;
|
||||
}
|
||||
});
|
||||
if (ris?.titoloOriginale) {
|
||||
// E' presente il vero titolo del Libro !
|
||||
titoloOriginale = ris.titoloOriginale;
|
||||
}
|
||||
|
||||
return { title, subtitle, pages, format, edition };
|
||||
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(isbn) {
|
||||
const html = await this.fetchPage(isbn);
|
||||
async scrapeISBN(myproduct, isbn, options) {
|
||||
const isbn10 = this.isbn13to10(isbn);
|
||||
const html = await this.fetchPageISBN10(isbn10);
|
||||
if (!html) return null;
|
||||
|
||||
const data = this.extractData(html);
|
||||
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) {
|
||||
async scrapeMultiple(isbnList, options) {
|
||||
const results = [];
|
||||
for (const isbn of isbnList) {
|
||||
console.log(`Scraping ISBN: ${isbn}`);
|
||||
const data = await this.scrapeISBN(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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function ScraperDataAmazon(idapp, options) {
|
||||
const scraper = new AmazonBookScraper();
|
||||
const isbn = options.isbn;
|
||||
generateHtmlTableFromObject(obj) {
|
||||
if (!obj || typeof obj !== 'object') return '';
|
||||
|
||||
try {
|
||||
const data = await scraper.scrapeISBN(isbn);
|
||||
console.log(data);
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return res.status(400).send({ code: server_constants.RIS_CODE_ERR, msg: '' });
|
||||
let html = '<table border="1" cellpadding="5" cellspacing="0" style="border-collapse: collapse;">';
|
||||
html += '<thead><tr><th>Chiave</th><th>Valore</th></tr></thead><tbody>';
|
||||
|
||||
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 += `<tr><td>${key}</td><td>${displayValue}</td></tr>`;
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function 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: '' });
|
||||
}
|
||||
}
|
||||
|
||||
export default AmazonBookScraper;
|
||||
module.exports = AmazonBookScraper;
|
||||
|
||||
Reference in New Issue
Block a user