Riempire il catalogo a mano costa caro e porta via troppo tempo, soprattutto quando il fornitore ha migliaia di referenze, con prezzi e stock che cambiano ogni settimana. La soluzione è uno scraper per PrestaShop: un programma che raccoglie i prodotti dal sito del fornitore (o dal suo listino) e li carica nel tuo negozio con nomi, descrizioni, foto, caratteristiche e — questo è il punto critico — con i prezzi ricalcolati correttamente.
In questo articolo vedremo come è organizzato PrestaShop dal punto di vista dell’importazione, quali soluzioni pronte esistono e scriveremo un nostro scraper, che raccoglie i dati dal sito di un fornitore e converte la valuta in parallelo, prendendo il tasso di cambio dal file giornaliero di riferimento della Banca Centrale Europea.
Perché contano la versione e l’architettura di PrestaShop
Come nelle altre piattaforme e-commerce serie, in PrestaShop è fortemente sconsigliato scrivere i prodotti direttamente nel database: la struttura di archiviazione è complessa (la scheda è distribuita su decine di tabelle: prodotto, traduzioni per lingua, prezzi, stock, combinazioni, immagini, categorie) e un INSERT diretto rompe quasi di sicuro indici, cache e logica di business. La strada corretta è lavorare attraverso i meccanismi nativi della piattaforma: l’importazione CSV del back office, l’API Web Service o le classi PHP del core.
A differenza dei prodotti commerciali con edizioni a pagamento, PrestaShop è software open source: esiste un’unica edizione, si scarica gratis e si installa sul proprio hosting. Il modello di business dell’ecosistema vive altrove: nel marketplace ufficiale Addons (addons.prestashop.com), dove PrestaShop e gli sviluppatori della community vendono moduli e temi, compresi i moduli di importazione dei cataloghi. Per questo, quando pianifichi uno scraper, la domanda non è «quale edizione ho?», ma «quale versione di PrestaShop uso e quali meccanismi di importazione ho a disposizione?».
Quello che pesa davvero è il ramo di versioni: la 1.6 è fuori supporto da anni, la 1.7 ha avviato la migrazione progressiva ai componenti Symfony e i rami attuali sono l’8 e il 9. Tutti condividono gli stessi concetti di catalogo, ma i moduli del marketplace vengono pubblicati per versioni specifiche: prima di scegliere una soluzione di importazione, verifica che sia compatibile con la tua versione di PrestaShop e con quella di PHP del tuo server.
Altre cose che uno scraper deve assolutamente capire di PrestaShop:
- Prodotti e categorie — ogni prodotto è agganciato a un albero di categorie e ha una categoria predefinita; i campi di testo (nome, descrizione, URL friendly) sono multilingua e vengono salvati lingua per lingua.
- Combinazioni (varianti) — le varianti di un prodotto (colore, taglia) sono modellate come combinazioni di attributi, con la propria reference, il proprio impatto sul prezzo e il proprio stock. Se il fornitore ha variazioni, lo scraper deve creare sia il prodotto sia le sue combinazioni.
- Multivaluta — PrestaShop supporta più valute e può aggiornare i tassi di cambio dal back office. Per importare i prezzi del fornitore, però, conviene fissare il prezzo base già convertito: noi prenderemo il tasso ufficiale dal file XML giornaliero della BCE (
eurofxref-daily.xml) e applicheremo il margine a parte, così i prezzi restano riproducibili. - Importazione CSV — il canale standard di caricamento massivo: in «Parametri avanzati → Importa» si caricano file di categorie, prodotti e combinazioni, con corrispondenza delle colonne configurabile e immagini via URL. È il punto di aggancio naturale per uno scraper.
Conclusione pratica: prima di sviluppare, fissa la versione di PrestaShop, l’albero delle categorie, lo schema di caratteristiche e attributi, e se si usano combinazioni e più lingue o valute. Da questo dipende tutta la logica di importazione.
Soluzioni pronte: cosa offre il mercato
Se il negozio è standard, può darsi che non serva programmare nulla di proprio.
Gli strumenti standard di PrestaShop
PrestaShop offre di serie due vie di caricamento:
- Importazione CSV — «Parametri avanzati → Importa»: accetta file di prodotti, categorie e combinazioni, permette di far corrispondere ogni colonna del file a un campo della scheda (reference, nome, descrizione, prezzo, tasse, immagini via URL) e di salvare quella configurazione di corrispondenze per i caricamenti successivi.
- API Web Service — una API REST integrata (si attiva in «Parametri avanzati → Web Service») con chiavi di accesso e permessi per risorsa: prodotti, categorie, stock, immagini. È pensata proprio per le integrazioni e le sincronizzazioni esterne.
Da sole, nessuna delle due «visita» siti altrui: hanno bisogno di un file o di richieste già pronti. Per questo di solito si abbinano a uno scraper: lo scraper raccoglie il CSV (o chiama l’API) e l’importazione standard lo carica. È la variante più sicura per il core.
I moduli del marketplace ufficiale Addons
Il marketplace Addons raccoglie migliaia di moduli, e l’importazione di cataloghi è una delle sue categorie classiche:
- Importatori avanzati di CSV/Excel/XML — moduli che estendono l’importatore standard: caricamenti pianificati da URL o FTP, corrispondenze più flessibili, aggiornamento selettivo di prezzi e stock, importazione di combinazioni, caratteristiche e immagini.
- Connettori per fornitori e dropshipping — moduli che sincronizzano il catalogo con i feed dei grossisti o con i formati di feed più diffusi, applicando regole di margine sul prezzo di costo.
Consiglio: molti moduli a pagamento offrono una demo o una versione di prova, e tutte le schede di Addons indicano la compatibilità di versione. Prova il modulo sul tuo fornitore concreto prima di comprare — non tutti i siti si prestano allo scraping «di fabbrica». E tieni presente che un modulo di importazione ha comunque bisogno di un file o di un feed di origine: la parte «percorrere il sito del fornitore» resta di solito fuori dal suo raggio d’azione.
Programmi esterni
- Scraper universali desktop e cloud (Octoparse, ParseHub, WebHarvy e simili) — configurano l’estrazione dal sito del fornitore in modo visuale ed esportano CSV o Excel, che poi passa dall’importatore standard di PrestaShop. Funzionano bene finché il sito di origine è semplice; conversione di valuta e regole di margine vanno risolte in un passaggio intermedio, per esempio con formule nel foglio di calcolo.
Quando le soluzioni pronte non bastano? Con un markup non standard o protezioni anti-bot sul sito di origine, con una logica di margini tutta tua, con la conversione di valuta a un tasso che controlli tu, con uno schema particolare di caratteristiche e combinazioni o con la sincronizzazione regolare dello stock. In quel caso si scrive uno scraper su misura — ed è quello che facciamo adesso.
Esempio 1: scraper in PHP con le classi di PrestaShop (con conversione di valuta)
Dato che PrestaShop è scritto in PHP, la via più diretta è uno script PHP che carica il core di PrestaShop e scrive i prodotti attraverso le sue classi. La conversione di valuta la faremo con il file di riferimento della BCE, e il margine lo applicheremo noi.
Il codice è didattico. Prima di lanciarlo su un negozio in produzione, fai un backup, assicurati che lo scraping del sito di origine non ne violi le condizioni né la legge, e prova su una copia.
Passo 0. Caricamento del core
Lo script si esegue da console o via cron. Basta includere config.inc.php: questo inizializza la configurazione, la connessione al database e l’autoload delle classi.
<?php
// perché lo script funzioni da console/cron e non solo in contesto web
require '/var/www/prestashop/config/config.inc.php';
// contesto minimo: negozio e lingua predefiniti
Shop::setContext(Shop::CONTEXT_SHOP, (int) Configuration::get('PS_SHOP_DEFAULT'));
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');Passo 1. Convertitore di valuta con il file della BCE
La Banca Centrale Europea pubblica ogni giorno lavorativo i tassi di riferimento dell’euro in un XML pubblico: eurofxref-daily.xml. Da lì prendiamo il tasso di cambio, convertiamo il prezzo del fornitore usando l’euro come valuta ponte e aggiungiamo il margine a parte.
<?php
class CurrencyConverter
{
private const ECB_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
/** @var array<string, float> tassi BCE: quante unità di valuta vale 1 EUR */
private array $rates = [];
public function __construct(
private string $from = 'USD',
private string $to = 'EUR',
private float $markup = 1.20 // margine del negozio: +20%
) {}
private function rate(string $currency): float
{
if ($currency === 'EUR') {
return 1.0;
}
if ($this->rates === []) {
$xml = simplexml_load_file(self::ECB_URL);
foreach ($xml->Cube->Cube->Cube as $cube) {
$this->rates[(string) $cube['currency']] = (float) $cube['rate'];
}
}
if (!isset($this->rates[$currency])) {
throw new RuntimeException("Nessun tasso BCE per {$currency}");
}
return $this->rates[$currency];
}
/** Prezzo del fornitore -> prezzo del negozio al tasso BCE + margine. */
public function convert(?float $amount): ?float
{
if ($amount === null) {
return null;
}
// la BCE quota tutto contro l'euro: passiamo per EUR come valuta ponte
$converted = $amount * $this->rate($this->to) / $this->rate($this->from);
return round($converted * $this->markup, 2);
}
}Passo 2. Scraper delle schede del fornitore
Per il parsing dell’HTML usiamo i nativi DOMDocument + DOMXPath e cURL, senza librerie di terze parti. I selettori sono fittizi: si adattano individualmente a ogni sito di origine.
<?php
class SupplierParser
{
public function __construct(
private string $baseUrl,
private CurrencyConverter $converter,
private float $delay = 1.0
) {}
private function getHtml(string $url): string
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_TIMEOUT => 20,
CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; CatalogImporter/1.0)',
]);
$html = curl_exec($ch);
if ($html === false) {
throw new RuntimeException('cURL: ' . curl_error($ch));
}
curl_close($ch);
return $html;
}
private function xpath(string $html): DOMXPath
{
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML('<?xml encoding="UTF-8">' . $html);
libxml_clear_errors();
return new DOMXPath($doc);
}
private function absolute(string $href): string
{
return str_starts_with($href, 'http')
? $href
: rtrim($this->baseUrl, '/') . '/' . ltrim($href, '/');
}
/** Link ai prodotti dalle pagine del catalogo con paginazione. */
public function parseCatalog(string $path, int $maxPages = 5): array
{
$urls = [];
for ($page = 1; $page <= $maxPages; $page++) {
$xp = $this->xpath($this->getHtml($this->absolute("{$path}?page={$page}")));
$links = $xp->query("//div[contains(@class,'product-card')]//a[contains(@class,'product-link')]");
if ($links->length === 0) {
break;
}
foreach ($links as $a) {
$urls[] = $this->absolute($a->getAttribute('href'));
}
usleep((int)($this->delay * 1_000_000));
}
return $urls;
}
/** Parsing di una scheda prodotto. */
public function parseProduct(string $url): array
{
$xp = $this->xpath($this->getHtml($url));
$text = fn(string $q) => trim($xp->query($q)->item(0)?->textContent ?? '');
$name = $text("//h1[contains(@class,'product-title')]") ?: 'Senza nome';
$sku = $text("//*[contains(@class,'sku')]");
$descr = $text("//*[contains(@class,'product-description')]");
$priceRaw = $text("//*[contains(@class,'price')]//*[contains(@class,'value')]");
$imgNode = $xp->query("//*[contains(@class,'product-gallery')]//img")->item(0);
$imageUrl = $imgNode ? $this->absolute($imgNode->getAttribute('src')) : '';
$priceSource = null;
if ($priceRaw !== '') {
$digits = preg_replace('/[^0-9.]/', '', str_replace(',', '.', $priceRaw));
$priceSource = $digits !== '' ? (float)$digits : null;
}
usleep((int)($this->delay * 1_000_000));
return [
'name' => $name,
'sku' => $sku,
'description' => $descr,
'image_url' => $imageUrl,
'price_source' => $priceSource,
'price' => $this->converter->convert($priceSource), // conversione di valuta
'source_url' => $url,
];
}
}Passo 3. Importazione nel catalogo con le classi di PrestaShop
Qui sta la differenza chiave rispetto a toccare il database: scriviamo attraverso Product (la scheda), StockAvailable (lo stock) e Image (le immagini), e PrestaShop si occupa di tabelle, cache e indici. I duplicati si tengono sotto controllo tramite il riferimento (reference), dove salviamo il codice articolo del fornitore.
<?php
class PrestaShopImporter
{
public function __construct(
private int $categoryId, // categoria predefinita
private int $idLang // lingua predefinita del negozio
) {}
/** Cerchiamo il prodotto per reference per non creare duplicati. */
private function findByReference(string $reference): ?int
{
$id = Db::getInstance()->getValue(
'SELECT id_product FROM ' . _DB_PREFIX_ . 'product WHERE reference = "' . pSQL($reference) . '"'
);
return $id ? (int) $id : null;
}
public function import(array $p): int
{
$reference = $p['sku'] !== '' ? $p['sku'] : md5($p['source_url']);
$existingId = $this->findByReference($reference);
$product = $existingId ? new Product($existingId) : new Product();
$product->reference = $reference; // reference = codice articolo del fornitore
$product->name = [$this->idLang => $p['name']];
$product->link_rewrite = [$this->idLang => Tools::str2url($p['name'])];
$product->description = [$this->idLang => $p['description']];
$product->id_category_default = $this->categoryId;
$product->price = $p['price']; // prezzo base tasse escluse, già in EUR
$product->active = true;
if ($existingId) {
// il prodotto esiste già: aggiorniamo campi e prezzo (senza toccare l'immagine)
$product->update();
} else {
if (!$product->add()) {
throw new RuntimeException('Product::add() fallito per ' . $reference);
}
$product->addToCategories([$this->categoryId]);
// scarichiamo e associamo l'immagine (se il sito di origine la fornisce)
if ($p['image_url'] !== '') {
$image = new Image();
$image->id_product = (int) $product->id;
$image->position = Image::getHighestPosition($product->id) + 1;
$image->cover = true; // la prima immagine diventa la copertina
if ($image->add()) {
AdminImportController::copyImg($product->id, $image->id, $p['image_url'], 'products', true);
}
}
}
// stock disponibile del prodotto (senza combinazioni: id_product_attribute = 0)
StockAvailable::setQuantity((int) $product->id, 0, 100);
return (int) $product->id;
}
}Passo 4. Punto di ingresso ed esecuzione via cron
<?php
// dopo il blocco di caricamento del core (passo 0) e le classi qui sopra:
$converter = new CurrencyConverter('USD', 'EUR', 1.25);
$parser = new SupplierParser('https://supplier-example.com', $converter, 1.0);
$importer = new PrestaShopImporter(categoryId: 47, idLang: $idLang);
$urls = $parser->parseCatalog('/catalog/category-1', maxPages: 3);
echo 'Prodotti trovati: ' . count($urls) . PHP_EOL;
$imported = 0;
foreach ($urls as $url) {
try {
$product = $parser->parseProduct($url);
if ($product['price'] === null) {
echo "Saltato (senza prezzo): {$url}" . PHP_EOL;
continue;
}
$id = $importer->import($product);
$imported++;
printf("[%d] %s — %s USD -> %s EUR (ID %d)%s",
$imported, $product['name'], $product['price_source'], $product['price'], $id, PHP_EOL);
} catch (Throwable $e) {
echo "Errore su {$url}: {$e->getMessage()}" . PHP_EOL;
}
}
echo "Fatto. Importati/aggiornati: {$imported}" . PHP_EOL;Per l’avvio automatico una volta al giorno, una riga nel crontab:
# ogni giorno alle 4:00 sincronizziamo il catalogo con il fornitore
0 4 * * * /usr/bin/php /var/www/prestashop/scripts/importer/run.php >> /var/www/prestashop/scripts/importer/importer.log 2>&1Se il tuo hosting non dà accesso al pianificatore di sistema, l’alternativa abituale è esporre lo script dietro un URL protetto da token e chiamarlo dal cron del pannello di hosting o da un servizio di cron esterno. Per le importazioni pesanti, in ogni caso, il cron di sistema è più affidabile: il carico non interferisce con le visite del negozio.
Esempio 2: raccolta in Python → CSV → importatore standard
Se non vuoi caricare il core né toccare il codice del negozio in produzione, puoi disaccoppiare lo scraping dall’importazione: raccogliere i dati con lo strumento che preferisci (per esempio Python), salvarli in un CSV e affidare il caricamento all’importatore standard di «Parametri avanzati → Importa». Così lo scraper non dipende affatto da PrestaShop né dai suoi aggiornamenti.
Qui la conversione di valuta è a carico nostro: prendiamo il tasso di riferimento della BCE e applichiamo il margine (come nella variante PHP, ma sul lato del raccoglitore).
import csv
import time
import requests
import xml.etree.ElementTree as ET
from bs4 import BeautifulSoup
from urllib.parse import urljoin
from datetime import date
class EcbRate:
"""Tasso di riferimento della BCE con cache giornaliera."""
URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
NS = {"e": "http://www.ecb.int/vocabulary/2002-08-01/eurofxref"}
def __init__(self, currency="USD"):
self.currency = currency
self._rate = None
self._date = None
def rate(self):
if self._rate is None or self._date != date.today():
r = requests.get(self.URL, timeout=15)
tree = ET.fromstring(r.text)
for cube in tree.findall(".//e:Cube[@currency]", self.NS):
if cube.attrib["currency"] == self.currency:
# la BCE pubblica quante unità di valuta vale 1 EUR
self._rate = float(cube.attrib["rate"])
self._date = date.today()
break
return self._rate
def parse_and_export(base_url, catalog_path, out_csv,
currency="USD", markup=1.25, max_pages=3, delay=1.0):
ecb = EcbRate(currency)
session = requests.Session()
session.headers["User-Agent"] = "Mozilla/5.0 (compatible; CatalogImporter/1.0)"
rows = []
for page in range(1, max_pages + 1):
soup = BeautifulSoup(
session.get(f"{base_url}{catalog_path}?page={page}", timeout=20).text, "lxml"
)
cards = soup.select(".product-card a.product-link")
if not cards:
break
for a in cards:
url = urljoin(base_url, a["href"])
ps = BeautifulSoup(session.get(url, timeout=20).text, "lxml")
name = ps.select_one("h1.product-title")
sku = ps.select_one(".sku")
descr = ps.select_one(".product-description")
price_el = ps.select_one(".price .value")
img = ps.select_one(".product-gallery img")
price_src = None
if price_el:
digits = "".join(c for c in price_el.text if c.isdigit() or c == ".")
price_src = float(digits) if digits else None
# conversione di valuta: prezzo / tasso BCE (1 EUR = X USD) * margine
price_eur = round(price_src / ecb.rate() * markup, 2) if price_src else ""
rows.append({
"Reference": sku.text.strip() if sku else url,
"Name": name.text.strip() if name else "Senza nome",
"Description": descr.decode_contents().strip() if descr else "",
"Price tax excluded": price_eur,
"Image URLs": urljoin(base_url, img["src"]) if img else "",
})
time.sleep(delay)
time.sleep(delay)
# CSV per l'importatore standard di PrestaShop (separatore ;)
with open(out_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys(), delimiter=";")
writer.writeheader()
writer.writerows(rows)
print(f"Fatto. Righe scritte: {len(rows)} -> {out_csv}")
if __name__ == "__main__":
parse_and_export(
base_url="https://supplier-example.com",
catalog_path="/catalog/category-1",
out_csv="import.csv",
)Poi il CSV si carica nel back office: Parametri avanzati → Importa, entità «Prodotti», separatore di campo ;; al secondo passaggio ogni colonna viene fatta corrispondere a un campo della scheda (reference, nome, descrizione, prezzo tasse escluse, URL dell’immagine), e quella corrispondenza può essere salvata per i caricamenti successivi. Vantaggio dell’approccio: lo scraper non dipende dal core di PrestaShop; svantaggio: il caricamento non è «al volo», ma un passaggio a parte.
Cosa conta in un progetto reale
Gli esempi didattici sono semplificati di proposito. In uno scraper di produzione per PrestaShop bisogna prevedere anche:
- Combinazioni — se il prodotto ha varianti (taglia, colore), creare gli attributi e le loro combinazioni, e agganciare prezzo e stock a ogni combinazione, non solo al prodotto base.
- Caratteristiche e attributi — far corrispondere le specifiche del sito di origine alle caratteristiche di PrestaShop e, se serve, creare automaticamente i valori nuovi.
- Categorie — generare l’albero delle categorie secondo la struttura del fornitore (con la classe
Categoryo con l’importazione CSV delle categorie). - Più lingue e valute — nei negozi multilingua, compilare i campi per ogni lingua; se vendi in più valute, decidere se convertire durante l’importazione o lasciare che PrestaShop ricalcoli con i propri tassi.
- Deduplicazione e sincronizzazione — aggiornare prezzo e stock per
referenceinvece di creare duplicati; così uno scraping una tantum diventa una sincronizzazione regolare. - Tasso di cambio e margine — salvarli nella configurazione e registrare nel log con quale tasso è stato ricalcolato ogni lotto, perché i prezzi siano riproducibili.
- Etica e legalità — rispettare
robots.txt, fare pause tra le richieste e tenere conto delle condizioni d’uso del sito di origine.
Conclusione
Per un negozio standard spesso basta la coppia «lo scraper genera un file + importazione CSV standard» o un modulo di importazione dal marketplace ufficiale Addons. Ma appena compaiono un sito di origine non standard, uno schema tutto tuo di caratteristiche e combinazioni, più lingue o valute, la conversione di valuta a un tasso ufficiale aggiornato o la sincronizzazione regolare dello stock, la cosa più affidabile è commissionare uno scraper su misura che scriva attraverso i meccanismi nativi di PrestaShop.
Ti serve lo scraping di negozi online?
Se devi popolare o sincronizzare un negozio su PrestaShop (o su qualsiasi altra piattaforma), raccogliere dati dai siti dei tuoi fornitori o configurare l’aggiornamento automatico dei prezzi con conversione di valuta, contattaci. Progetteremo e implementeremo uno scraper su misura per la tua versione di PrestaShop e per i tuoi obiettivi.