CMS e plataformas 14 min de leitura

Scraper e importador para PrestaShop: soluções prontas e exemplo com conversão de moedas

Scraper para lojas PrestaShop: como a plataforma é organizada, quais módulos de importação o marketplace Addons oferece e um exemplo com conversão de moedas.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 16 fevereiro 2025

Preencher o catálogo à mão custa caro e leva tempo demais, sobretudo quando o fornecedor tem milhares de referências e os preços e o estoque mudam toda semana. A solução é um scraper para PrestaShop: um programa que coleta os produtos do site do fornecedor (ou da lista de preços dele) e os carrega na sua loja com nomes, descrições, fotos, características e — isto é o ponto crítico — com os preços corretamente recalculados.

Neste artigo vamos ver como o PrestaShop é organizado do ponto de vista da importação, quais soluções prontas existem e vamos escrever nosso próprio scraper, que coleta os dados do site de um fornecedor e converte a moeda em paralelo, usando a taxa do arquivo diário de referência do Banco Central Europeu.


Por que a versão e a arquitetura do PrestaShop importam

Como em outras plataformas sérias de e-commerce, no PrestaShop não se recomenda de forma alguma gravar os produtos diretamente no banco de dados: a estrutura de armazenamento é complexa (o cadastro se espalha por dezenas de tabelas: produto, traduções por idioma, preços, estoque, combinações, imagens, categorias) e um INSERT direto quase certamente quebra índices, caches e lógica de negócio. O caminho correto é trabalhar pelos mecanismos da própria plataforma: a importação CSV do back office, a API Web Service ou as classes PHP do núcleo.

Diferentemente dos produtos comerciais com edições pagas, o PrestaShop é um software de código aberto: há uma única edição, baixada gratuitamente e instalada na sua própria hospedagem. O modelo de negócio do ecossistema vive em outro lugar: no marketplace oficial Addons (addons.prestashop.com), onde o PrestaShop e os desenvolvedores da comunidade vendem módulos e temas, incluindo os módulos de importação de catálogos. Por isso, ao planejar um scraper, a pergunta não é «qual edição eu tenho?», e sim «qual versão do PrestaShop eu uso e quais mecanismos de importação tenho disponíveis?».

O que pesa de verdade é a linha de versões: a 1.6 está descontinuada há anos, a 1.7 iniciou a migração gradual para componentes do Symfony e as linhas atuais são a 8 e a 9. As três compartilham os mesmos conceitos de catálogo, mas os módulos do marketplace são publicados para versões específicas: antes de escolher uma solução de importação, verifique se ela é compatível com a sua versão do PrestaShop e com a versão de PHP do seu servidor.

Mais algumas coisas que o scraper é obrigado a entender sobre o PrestaShop:

  • Produtos e categorias — cada produto pertence a uma árvore de categorias e tem uma categoria padrão; os campos de texto (nome, descrição, URL amigável) são multilíngues e são gravados por idioma.
  • Combinações (variantes) — as variantes de um produto (cor, tamanho) são modeladas como combinações de atributos, com referência própria, impacto próprio no preço e estoque próprio. Se o fornecedor tem variações, o scraper precisa criar tanto o produto quanto as suas combinações.
  • Multimoeda — o PrestaShop suporta várias moedas e pode atualizar as taxas de câmbio pelo back office. Ainda assim, para importar preços de fornecedor vale fixar o preço base já convertido: nós vamos usar a taxa oficial do arquivo XML diário do BCE (eurofxref-daily.xml) e aplicar a margem à parte, para que os preços sejam reproduzíveis.
  • Importação CSV — o canal padrão de carga em massa: em «Parâmetros Avançados → Importar» são enviados arquivos de categorias, produtos e combinações, com correspondência de colunas configurável e imagens por URL. É o ponto de encaixe natural para um scraper.

Conclusão prática: antes de desenvolver, defina a versão do PrestaShop, a árvore de categorias, o esquema de características e atributos, e se serão usadas combinações e vários idiomas ou moedas. Toda a lógica de importação depende disso.


Soluções prontas: o que há no mercado

Se a loja é típica, talvez nem seja preciso programar nada próprio.

Ferramentas padrão do PrestaShop

O PrestaShop traz de fábrica dois caminhos de carga:

  • Importação CSV — «Parâmetros Avançados → Importar»: aceita arquivos de produtos, categorias e combinações, permite mapear cada coluna do arquivo para um campo do cadastro (referência, nome, descrição, preço, impostos, imagens por URL) e salvar essa configuração de correspondências para cargas repetidas.
  • API Web Service — uma API REST integrada (ativada em «Parâmetros Avançados → Web Service») com chaves de acesso e permissões por recurso: produtos, categorias, estoque, imagens. Foi pensada exatamente para integrações e sincronizações externas.

Sozinho, nenhum dos dois «visita» sites de terceiros: eles precisam de um arquivo ou de requisições já preparados. Por isso costumam ser combinados com um scraper: o scraper coleta o CSV (ou chama a API) e a importação padrão faz a carga. É a variante mais segura para o núcleo.

Módulos do marketplace oficial Addons

O marketplace Addons reúne milhares de módulos, e a importação de catálogos é uma de suas categorias clássicas:

  • Importadores avançados de CSV/Excel/XML — módulos que ampliam o importador padrão: cargas agendadas a partir de uma URL ou FTP, mapeamentos mais flexíveis, atualização seletiva de preços e estoque, importação de combinações, características e imagens.
  • Conectores com fornecedores e dropshipping — módulos que sincronizam o catálogo com os feeds de atacadistas ou com formatos de feed comuns, aplicando regras de margem sobre o preço de custo.

Dica: muitos módulos pagos oferecem demo ou versão de teste, e todas as páginas do Addons indicam a compatibilidade de versões. Teste o módulo contra o seu fornecedor específico antes de comprar — nem de longe todos os sites são raspados «de fábrica». E lembre que um módulo de importação continua precisando de um arquivo ou feed de origem: a parte de «percorrer o site do fornecedor» costuma ficar fora do alcance dele.

Programas externos

  • Scrapers universais de desktop e na nuvem (Octoparse, ParseHub, WebHarvy e similares) — configuram a extração do site do fornecedor de forma visual e exportam CSV ou Excel, que depois passa pelo importador padrão do PrestaShop. Funcionam bem enquanto o site de origem for simples; a conversão de moedas e as regras de margem precisarão ser resolvidas em uma etapa intermediária, por exemplo com fórmulas na planilha.

Quando o pronto não basta? Com marcação fora do padrão ou proteções anti-bot no site de origem, com uma lógica própria de margens, com conversão de moedas a uma taxa controlada por você, com um esquema particular de características e combinações ou com sincronização regular de estoque. Nesses casos escreve-se um scraper sob medida — e é disso que vamos tratar.


Exemplo 1: scraper em PHP com as classes do PrestaShop (com conversão de moedas)

Como o PrestaShop é escrito em PHP, o caminho mais direto é um script PHP que carrega o núcleo do PrestaShop e grava os produtos através das classes dele. A conversão de moedas será feita com o arquivo de referência do BCE, e a margem aplicaremos nós mesmos.

O código é didático. Antes de rodá-lo contra uma loja em produção, faça um backup, verifique se raspar o site de origem não viola os termos dele nem a lei, e teste em uma cópia.

Passo 0. Carga do núcleo

O script roda pelo console ou via cron. Basta incluir config.inc.php: isso inicializa a configuração, a conexão com o banco de dados e o autoload das classes.

php
<?php
// para o script funcionar pelo console/cron e não apenas no contexto web
require '/var/www/prestashop/config/config.inc.php';

// contexto mínimo: loja e idioma padrão
Shop::setContext(Shop::CONTEXT_SHOP, (int) Configuration::get('PS_SHOP_DEFAULT'));
$idLang = (int) Configuration::get('PS_LANG_DEFAULT');

Passo 1. Conversor de moedas com o arquivo do BCE

O Banco Central Europeu publica em cada dia útil as taxas de referência do euro em um XML público: eurofxref-daily.xml. Pegamos dali a taxa de câmbio, convertemos o preço do fornecedor usando o euro como moeda-ponte e adicionamos a margem separadamente.

php
<?php
class CurrencyConverter
{
    private const ECB_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';

    /** @var array<string, float> taxas do BCE: quantas unidades da moeda vale 1 EUR */
    private array $rates = [];

    public function __construct(
        private string $from = 'USD',
        private string $to = 'EUR',
        private float $markup = 1.20   // margem da loja: +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("Não há taxa do BCE para {$currency}");
        }
        return $this->rates[$currency];
    }

    /** Preço do fornecedor -> preço da loja pela taxa do BCE + margem. */
    public function convert(?float $amount): ?float
    {
        if ($amount === null) {
            return null;
        }
        // o BCE cota tudo contra o euro: passamos pelo EUR como moeda-ponte
        $converted = $amount * $this->rate($this->to) / $this->rate($this->from);
        return round($converted * $this->markup, 2);
    }
}

Passo 2. Scraper dos cadastros do fornecedor

Para fazer o parsing do HTML usamos os nativos DOMDocument + DOMXPath e cURL, sem bibliotecas de terceiros. Os seletores são fictícios: eles são ajustados individualmente para cada site de origem.

php
<?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, '/');
    }

    /** Links de produto a partir das páginas de catálogo com paginação. */
    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 de um cadastro de produto. */
    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')]") ?: 'Sem 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), // conversão de moeda
            'source_url'   => $url,
        ];
    }
}

Passo 3. Importação para o catálogo com as classes do PrestaShop

Aqui está a diferença-chave em relação a mexer no banco de dados: gravamos através de Product (o cadastro), StockAvailable (o estoque) e Image (as imagens), e o PrestaShop cuida das tabelas, dos caches e dos índices. As duplicatas são controladas pela referência (reference), onde guardamos o código do fornecedor.

php
<?php
class PrestaShopImporter
{
    public function __construct(
        private int $categoryId,   // categoria padrão
        private int $idLang        // idioma padrão da loja
    ) {}

    /** Procuramos o produto pela referência para não gerar duplicatas. */
    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;   // referência = código do fornecedor
        $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'];  // preço base sem impostos, já em EUR
        $product->active              = true;

        if ($existingId) {
            // o produto já existe: atualizamos os campos e o preço (sem mexer na imagem)
            $product->update();
        } else {
            if (!$product->add()) {
                throw new RuntimeException('Product::add() falhou para ' . $reference);
            }
            $product->addToCategories([$this->categoryId]);

            // baixamos e associamos a imagem (se o site de origem a oferece)
            if ($p['image_url'] !== '') {
                $image = new Image();
                $image->id_product = (int) $product->id;
                $image->position   = Image::getHighestPosition($product->id) + 1;
                $image->cover      = true;   // a primeira imagem vira a capa
                if ($image->add()) {
                    AdminImportController::copyImg($product->id, $image->id, $p['image_url'], 'products', true);
                }
            }
        }

        // estoque disponível do produto (sem combinações: id_product_attribute = 0)
        StockAvailable::setQuantity((int) $product->id, 0, 100);

        return (int) $product->id;
    }
}

Passo 4. Ponto de entrada e execução via cron

php
<?php
// depois do bloco de carga do núcleo (passo 0) e das classes anteriores:

$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 'Produtos encontrados: ' . count($urls) . PHP_EOL;

$imported = 0;
foreach ($urls as $url) {
    try {
        $product = $parser->parseProduct($url);
        if ($product['price'] === null) {
            echo "Ignorado (sem preço): {$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 "Erro em {$url}: {$e->getMessage()}" . PHP_EOL;
    }
}

echo "Pronto. Importados/atualizados: {$imported}" . PHP_EOL;

Para o disparo automático uma vez por dia, uma linha no crontab:

bash
# todo dia às 4:00 sincronizamos o catálogo com o fornecedor
0 4 * * * /usr/bin/php /var/www/prestashop/scripts/importer/run.php >> /var/www/prestashop/scripts/importer/importer.log 2>&1

Se a sua hospedagem não dá acesso ao agendador de tarefas do sistema, a alternativa comum é expor o script atrás de uma URL protegida por token e chamá-lo pelo cron do painel da hospedagem ou por um serviço de cron externo. Para importações pesadas, em todo caso, o cron de sistema é mais confiável: a carga não interfere nas visitas da loja.


Exemplo 2: coleta em Python → CSV → importador padrão

Se você não quer carregar o núcleo nem tocar no código da loja em produção, dá para desacoplar o scraping da importação: coletar os dados com a ferramenta que preferir (por exemplo, Python), salvá-los em um CSV e deixar a carga por conta do importador padrão de «Parâmetros Avançados → Importar». Assim o scraper não depende em nada do PrestaShop nem das atualizações dele.

Aqui a conversão de moedas fica por nossa conta: usamos a taxa de referência do BCE e aplicamos a margem (igual à variante em PHP, mas do lado do coletor).

python
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:
    """Taxa de referência do BCE com cache por dia."""
    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:
                    # o BCE publica quantas unidades da moeda 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

            # conversão de moeda: preço / taxa do BCE (1 EUR = X USD) * margem
            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 "Sem 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 para o importador padrão do PrestaShop (separador ;)
    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"Pronto. Linhas gravadas: {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",
    )

Depois, o CSV é enviado no back office: Parâmetros Avançados → Importar, entidade «Produtos», separador de campos ;; no segundo passo, cada coluna é mapeada para um campo do cadastro (referência, nome, descrição, preço sem impostos, URL da imagem), e esse mapeamento pode ser salvo para as cargas seguintes. Vantagem da abordagem: o scraper não depende do núcleo do PrestaShop; desvantagem: a carga não é «em tempo real», e sim uma etapa à parte.


O que importa em um projeto real

Os exemplos didáticos são simplificados de propósito. Em um scraper de produção para PrestaShop é preciso prever ainda:

  • Combinações — se o produto tem variantes (tamanho, cor), criar os atributos e as suas combinações, e pendurar o preço e o estoque em cada combinação, não apenas no produto base.
  • Características e atributos — mapear as especificações do site de origem para as características do PrestaShop e, se necessário, criar automaticamente os valores novos.
  • Categorias — gerar a árvore de categorias conforme a estrutura do fornecedor (com a classe Category ou com a importação CSV de categorias).
  • Vários idiomas e moedas — em lojas multilíngues, preencher os campos para cada idioma; se você vende em várias moedas, decidir se converte durante a importação ou se deixa o PrestaShop recalcular com as próprias taxas.
  • Deduplicação e sincronização — atualizar preço e estoque pela reference em vez de criar duplicatas; assim um scraping pontual vira uma sincronização regular.
  • Taxa de câmbio e margem — guardá-las na configuração e registrar no log com qual taxa cada lote foi recalculado, para que os preços sejam reproduzíveis.
  • Ética e legalidade — respeitar o robots.txt, fazer pausas entre as requisições e considerar os termos de uso do site de origem.

Conclusão

Para uma loja típica costuma bastar a dupla «o scraper gera um arquivo + importação CSV padrão» ou um módulo de importação do marketplace oficial Addons. Mas assim que aparecem um site de origem fora do padrão, um esquema próprio de características e combinações, vários idiomas ou moedas, a conversão de moedas com uma taxa oficial atualizada ou a sincronização regular de estoque, o mais confiável é encomendar um scraper sob medida que grave pelos mecanismos nativos do PrestaShop.


Precisa de scraping de lojas online?

Se você precisa popular ou sincronizar uma loja em PrestaShop (ou em qualquer outra plataforma), coletar dados dos sites dos seus fornecedores ou configurar a atualização automática de preços com conversão de moedas, escreva para nós. Vamos projetar e implantar um scraper adequado à sua versão do PrestaShop e às suas tarefas.