Poblar una tienda online a mano es la parte más cara y tediosa del lanzamiento. Cuando el proveedor maneja miles de referencias y los precios cambian cada semana, no hay forma de arreglárselas sin automatización. Aquí entra en escena el scraper para OpenCart: un programa que recopila los datos de productos de un sitio de origen (proveedor, distribuidor, marketplace) y los vuelca en su tienda con nombres, descripciones, fotografías, características y, lo que es especialmente importante, con los precios correctamente convertidos.
En este artículo veremos en qué se diferencian las versiones de OpenCart desde el punto de vista del scraping, qué soluciones listas existen y escribiremos nuestro propio scraper en Python que recopila productos del sitio del proveedor y convierte la divisa en paralelo.
Por qué la versión de OpenCart importa
Al final, el scraper escribe los datos en la base o genera un archivo de importación, y la estructura de la base y la lógica de las URL SEO varían entre versiones de OpenCart. Ignorarlo es la garantía de acabar con productos «rotos» o errores de importación.
OpenCart apareció allá por 2005 y desde entonces ha pasado por varias reescrituras de calado: la versión 1.5 (2011) trajo un nuevo editor de layouts y un nuevo panel de administración; la rama 2.x (2014), una arquitectura modular y un sistema de extensiones mejorado; la versión 3.0 (2017), el modo multitienda y el Marketplace; y la rama 4.x pasó a ser la actual, con la base de código renovada y el regreso de OCMOD. Todas estas ramas siguen apareciendo en proyectos vivos.
Esto es lo que el scraper debe tener en cuenta:
OpenCart 1.5.x: rama obsoleta, pero que todavía se encuentra. Estructura de tablas sencilla, aunque con nombres de campos distintos en varias tablas. Los módulos de importación listos apenas se mantienen ya para ella.
OpenCart 2.x: la rama «antigua» en producción más extendida. Aquí viven muchos de los módulos de importación más contrastados. Las URL SEO se guardan en la tabla url_alias.
OpenCart 3.x: la versión más popular en tiendas en producción en el momento de escribir esto. Estructura cercana a 2.x; las URL SEO, también en url_alias. La mayoría de los scrapers y módulos listos están pensados precisamente para 2.x--3.x.
OpenCart 4.x: la rama actual. Trae un cambio de fondo: las URL SEO se mudaron de url_alias a la tabla seo_url (vinculadas a store_id y language_id), y parte de los controladores también cambió. Un scraper escrito para OC3 no dejará URL amigables correctas en OC4 sin retoques.
Forks y ensamblajes basados en OpenCart: en el mercado circulan distribuciones modificadas de OpenCart con plantillas y módulos preinstalados. Su estructura de datos coincide con la versión de OpenCart correspondiente, pero los módulos «de serie» a veces se comportan de otra manera, así que conviene probarlos siempre por separado.
Conclusión práctica: antes de escribir o comprar un scraper, fije la versión exacta de la tienda (visible en el pie del panel de administración o en el
index.phpdel núcleo) y el prefijo de las tablas de la base (por defectooc_, aunque a menudo se cambia por otro).
Soluciones listas: qué ofrece el mercado
Si el presupuesto es ajustado y la tienda es de corte estándar, puede que ni siquiera haga falta escribir un scraper propio. Herramientas listas no faltan, y se dividen en varias categorías.
Módulos de importación dentro de OpenCart
Son extensiones que no «recorren» por sí mismas sitios ajenos, sino que reciben un archivo ya preparado (CSV / XLS / XLSX / XML) y lo cargan en el catálogo:
- Export/Import Tool — módulo gratuito y muy extendido; asume importaciones de hasta varios miles de productos (los límites dependen del hosting). Una buena opción para empezar.
- Módulos comerciales de importación CSV del Marketplace oficial de OpenCart — de pago, con licencia por dominio, y más flexibles a la hora de configurar la correspondencia de campos.
- Extensiones multiformato — módulos comerciales capaces de importar CSV, XLS, XLSX y XML de una sola vez, orientados a OC 2--3.
- Módulos de importación masiva por cola AJAX — cargan y actualizan catálogos grandes por lotes, sin chocar con los límites de tiempo de ejecución del servidor.
La combinación «el scraper genera el archivo → el módulo de importación lo sube» es la más fiable: usted revisa los datos con sus propios ojos antes de cargarlos y no toca nada directamente en la base.
Módulos integrales: scraper e importador en uno
Son soluciones «todo en uno» dentro del ecosistema de extensiones de OpenCart: ellas mismas scrapean los sitios de origen y cargan el resultado en la tienda. Dos perfiles típicos:
- Módulos con scraper integrado — extracción multihilo de los sitios de origen, traducción automática de textos, generación de descripciones con IA, planificador de tareas y mapeo flexible de datos; lo habitual es una licencia anual por dominio.
- Módulos de importación masiva avanzada — crean el árbol de categorías, cargan miles de productos con descripciones, opciones, atributos y fotos, y saben estandarizar los atributos «al vuelo».
Programas externos y servicios en la nube
- Octoparse — herramienta visual de scraping, de escritorio y en la nube, con decenas de plantillas listas para sitios populares; recopila datos de tiendas y marketplaces y los exporta a CSV/Excel para pasarlos después por un módulo de importación.
- Apify — plataforma de scraping en la nube con «actors» listos para sitios conocidos, ejecuciones programadas y exportación vía API; encaja bien cuando el catálogo del proveedor debe sincronizarse con regularidad.
¿Cuándo no basta una solución lista? Cuando el sitio del proveedor tiene un maquetado poco trivial o protección anti-scraping, cuando hace falta una lógica de márgenes específica, la conversión de divisas con su propio tipo de cambio, el encaje en su propia estructura de categorías o la sincronización periódica de stock. En esos casos se escribe un scraper a medida — y a eso vamos ahora.
Ejemplo: un scraper propio con importación y conversión de divisas
Analicemos un scraper didáctico en Python que:
- recorre el catálogo del proveedor y recopila las fichas de producto;
- extrae el nombre, el precio, la descripción, la referencia (SKU) y la imagen;
- convierte el precio de la divisa del proveedor a la divisa de la tienda con el tipo de cambio actual y el margen configurado;
- importa los productos directamente en la base de datos de OpenCart.
El código tiene fines didácticos. Antes de ejecutarlo en una tienda en producción, haga sin falta una copia de seguridad de la base, compruebe que el scraping del sitio de origen no vulnera sus condiciones de uso ni la ley, y pruébelo todo en una copia de la tienda.
Stack y preparación
pip install requests beautifulsoup4 pymysql lxmlrequests+beautifulsoup4— descarga y parseo del HTML;pymysql— escritura en la base de OpenCart;- el tipo de cambio lo tomamos de una fuente pública (en el ejemplo, el feed XML de tipos de referencia del Banco Central Europeo; puede sustituirse por cualquier otra).
Paso 1. Conversor de divisas
Primero, un módulo aparte que obtiene el tipo de cambio y recalcula el precio con el margen. El tipo se cachea para no llamar a la fuente con cada producto.
import requests
import xml.etree.ElementTree as ET
from datetime import date
class CurrencyConverter:
"""Obtiene el tipo de referencia del BCE y convierte el precio del proveedor a la divisa de la tienda."""
ECB_URL = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"
def __init__(self, source_currency="USD", markup=1.20):
# markup=1.20 — margen de la tienda: +20% sobre el tipo de cambio
self.source_currency = source_currency
self.markup = markup
self._rate = None
self._rate_date = None
def _fetch_rate(self):
"""Busca en el XML del BCE el tipo de la divisa frente al euro."""
resp = requests.get(self.ECB_URL, timeout=15)
tree = ET.fromstring(resp.text)
for cube in tree.iter():
if cube.get("currency") == self.source_currency:
return float(cube.get("rate")) # unidades de divisa por 1 EUR
raise ValueError(f"Divisa {self.source_currency} no encontrada en la respuesta del BCE")
@property
def rate(self):
# cacheamos el tipo de cambio para la fecha actual
if self._rate is None or self._rate_date != date.today():
self._rate = self._fetch_rate()
self._rate_date = date.today()
return self._rate
def convert(self, amount):
"""Precio del proveedor -> precio de la tienda (en euros) con el margen."""
if amount is None:
return None
price = float(amount) / self.rate * self.markup
return round(price, 2)Paso 2. Scraper de fichas del proveedor
Los selectores (.product-card, .price, etc.) son orientativos: se ajustan individualmente para cada sitio de origen tras revisar el código fuente de la página.
import time
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
class SupplierParser:
def __init__(self, base_url, converter, delay=1.0):
self.base_url = base_url
self.converter = converter
self.delay = delay # pausa entre peticiones para no «tumbar» el sitio de origen
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (compatible; CatalogImporter/1.0)"
})
def _get_soup(self, url):
resp = self.session.get(url, timeout=20)
resp.raise_for_status()
return BeautifulSoup(resp.text, "lxml")
def parse_catalog(self, catalog_path, max_pages=5):
"""Recopila los enlaces a productos de las páginas del catálogo con paginación."""
product_urls = []
for page in range(1, max_pages + 1):
url = urljoin(self.base_url, f"{catalog_path}?page={page}")
soup = self._get_soup(url)
cards = soup.select(".product-card a.product-link")
if not cards:
break # no hay más páginas
for a in cards:
product_urls.append(urljoin(self.base_url, a["href"]))
time.sleep(self.delay)
return product_urls
def parse_product(self, url):
"""Procesa una ficha de producto."""
soup = self._get_soup(url)
name = soup.select_one("h1.product-title")
price_raw = soup.select_one(".price .value")
description = soup.select_one(".product-description")
sku = soup.select_one(".sku")
image = soup.select_one(".product-gallery img")
# limpiamos el precio de espacios y símbolos de divisa
price_source = None
if price_raw:
digits = "".join(ch for ch in price_raw.text if ch.isdigit() or ch == ".")
price_source = float(digits) if digits else None
time.sleep(self.delay)
return {
"name": name.text.strip() if name else "Sin nombre",
"sku": sku.text.strip() if sku else "",
"description": description.decode_contents().strip() if description else "",
"image_url": urljoin(self.base_url, image["src"]) if image else "",
"price_source": price_source,
# aquí es donde se produce la conversión de divisas:
"price": self.converter.convert(price_source),
"source_url": url,
}Paso 3. Importación a la base de datos de OpenCart
Aquí aplicamos lo dicho al principio: la estructura depende de la versión. El ejemplo está pensado para OpenCart 3.x; la diferencia clave para OC4 va señalada en un comentario.
import pymysql
from datetime import datetime
from slugify import slugify # pip install python-slugify
class OpenCartImporter:
def __init__(self, db_config, prefix="oc_", store_id=0, language_id=1):
self.conn = pymysql.connect(**db_config, charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor)
self.prefix = prefix
self.store_id = store_id
self.language_id = language_id
def _seo_keyword(self, name):
return slugify(name) or "product"
def import_product(self, p, category_id=None):
cur = self.conn.cursor()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
px = self.prefix
# 1) tabla principal del producto
cur.execute(f"""
INSERT INTO {px}product
(model, sku, quantity, price, image, status,
date_added, date_modified, date_available)
VALUES (%s, %s, %s, %s, %s, 1, %s, %s, %s)
""", (p["sku"] or p["name"][:64], p["sku"], 100,
p["price"], "", now, now, now))
product_id = cur.lastrowid
# 2) descripción (nombre, texto, metadatos) — para cada idioma
cur.execute(f"""
INSERT INTO {px}product_description
(product_id, language_id, name, description,
meta_title, meta_description)
VALUES (%s, %s, %s, %s, %s, %s)
""", (product_id, self.language_id, p["name"],
p["description"], p["name"], p["name"]))
# 3) vínculo con la tienda
cur.execute(f"""
INSERT INTO {px}product_to_store (product_id, store_id)
VALUES (%s, %s)
""", (product_id, self.store_id))
# 4) vínculo con la categoría (si se indica)
if category_id:
cur.execute(f"""
INSERT INTO {px}product_to_category (product_id, category_id)
VALUES (%s, %s)
""", (product_id, category_id))
# 5) SEO URL (URL amigable)
keyword = f"{self._seo_keyword(p['name'])}-{product_id}"
# — OpenCart 3.x: tabla url_alias ---
cur.execute(f"""
INSERT INTO {px}url_alias (query, keyword)
VALUES (%s, %s)
""", (f"product_id={product_id}", keyword))
# — En OpenCart 4.x sería así (tabla seo_url):
# cur.execute(f'''INSERT INTO {px}seo_url
# (store_id, language_id, key, value, keyword)
# VALUES (%s, %s, "product_id", %s, %s)''',
# (self.store_id, self.language_id, product_id, keyword))
self.conn.commit()
return product_id
def close(self):
self.conn.close()Paso 4. Lo unimos todo
def main():
converter = CurrencyConverter(source_currency="USD", markup=1.25)
parser = SupplierParser("https://supplier-example.com", converter, delay=1.0)
importer = OpenCartImporter(
db_config={
"host": "localhost",
"user": "db_user",
"password": "db_password",
"database": "opencart_db",
},
prefix="oc_",
store_id=0,
language_id=1,
)
product_urls = parser.parse_catalog("/catalog/category-1", max_pages=3)
print(f"Productos encontrados: {len(product_urls)}")
imported = 0
for url in product_urls:
try:
product = parser.parse_product(url)
if product["price"] is None:
print(f"Omitido (sin precio): {url}")
continue
pid = importer.import_product(product, category_id=20)
imported += 1
print(f"[{imported}] {product['name']} — "
f"{product['price_source']} {converter.source_currency} "
f"-> {product['price']} € (ID {pid})")
except Exception as e:
print(f"Error en {url}: {e}")
importer.close()
print(f"Listo. Importados: {imported}")
if __name__ == "__main__":
main()Qué queda fuera del ejemplo y es importante en un proyecto real
El ejemplo didáctico está simplificado a propósito. En un scraper en producción hay que prever, además:
- La descarga y vinculación de imágenes — bajar las fotos a
image/catalog/...y registrar la ruta enproduct.imageyoc_product_image. A menudo los sitios de origen devuelven 403 ante la descarga directa, y hacen falta un referer y unas cabeceras correctos. - La deduplicación — comprobar por
sku/modelsi el producto ya existe en la base y actualizar su precio y stock en lugar de multiplicar duplicados. Esto mismo convierte un scraping puntual en una sincronización periódica. - Atributos y opciones —
oc_product_attributeyoc_product_option, con su propia estructura de relaciones. - Categorías — creación automática del árbol de categorías según la estructura del sitio de origen (
oc_category,oc_category_description,oc_category_path). - La limpieza de la caché de OpenCart y la regeneración de los índices SEO tras una importación masiva.
- El tipo de cambio y el margen — guardarlos en la configuración y registrar en los logs con qué tipo se recalculó cada lote, para que los precios sean reproducibles y transparentes.
- Ética y legalidad — respetar el
robots.txt, mantener pausas razonables entre peticiones y cumplir las condiciones de uso del sitio de origen.
El mismo scraper en PHP — directamente en el servidor de la tienda
Python es cómodo, pero exige un entorno aparte. OpenCart, en cambio, está escrito en PHP y funciona sobre MySQL, así que el scraper puede construirse con ese mismo stack y ejecutarse directamente en el hosting de la tienda, sin instalar Python ni runtimes de terceros. Ventajas de este enfoque:
- no hay que instalar nada adicional: PHP ya está en el servidor;
- se puede reutilizar el
config.phpde OpenCart para no duplicar las credenciales de la base; - es fácil programarlo con cron para una sincronización periódica;
- si se desea, el script puede engancharse al núcleo de OpenCart y escribir los productos a través de sus modelos, en lugar de atacar las tablas directamente.
Para el parseo del HTML usamos los nativos DOMDocument + DOMXPath (sin bibliotecas de terceros); para las peticiones, cURL; para la base, PDO.
Paso 1. Conversor de divisas
<?php
class CurrencyConverter
{
private const ECB_URL = 'https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
private ?float $rate = null;
private ?string $rateDate = null;
public function __construct(
private string $sourceCurrency = 'USD',
private float $markup = 1.20 // margen de la tienda: +20% sobre el tipo de cambio
) {}
private function fetchRate(): float
{
$xmlRaw = file_get_contents(self::ECB_URL);
$xml = new SimpleXMLElement($xmlRaw);
$xml->registerXPathNamespace('e', 'http://www.ecb.int/vocabulary/2002-08-01/eurofxref');
foreach ($xml->xpath('//e:Cube[@currency]') as $cube) {
if ((string)$cube['currency'] === $this->sourceCurrency) {
return (float)$cube['rate']; // unidades de divisa por 1 EUR
}
}
throw new RuntimeException("Divisa {$this->sourceCurrency} no encontrada en la respuesta del BCE");
}
public function getRate(): float
{
$today = date('Y-m-d');
if ($this->rate === null || $this->rateDate !== $today) {
$this->rate = $this->fetchRate();
$this->rateDate = $today;
}
return $this->rate;
}
public function convert(?float $amount): ?float
{
if ($amount === null) {
return null;
}
return round($amount / $this->getRate() * $this->markup, 2);
}
}Paso 2. Scraper de fichas del proveedor
<?php
class SupplierParser
{
public function __construct(
private string $baseUrl,
private CurrencyConverter $converter,
private float $delay = 1.0 // pausa entre peticiones, en segundos
) {}
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 makeXPath(string $html): DOMXPath
{
$doc = new DOMDocument();
libxml_use_internal_errors(true); // silenciamos los errores del HTML «sucio»
$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, '/');
}
/** Recopila los enlaces a productos de las páginas del catálogo con paginación. */
public function parseCatalog(string $catalogPath, int $maxPages = 5): array
{
$urls = [];
for ($page = 1; $page <= $maxPages; $page++) {
$html = $this->getHtml($this->absolute("{$catalogPath}?page={$page}"));
$xpath = $this->makeXPath($html);
$links = $xpath->query("//div[contains(@class,'product-card')]//a[contains(@class,'product-link')]");
if ($links->length === 0) {
break; // no hay más páginas
}
foreach ($links as $a) {
$urls[] = $this->absolute($a->getAttribute('href'));
}
usleep((int)($this->delay * 1_000_000));
}
return $urls;
}
/** Procesa una ficha de producto. */
public function parseProduct(string $url): array
{
$xpath = $this->makeXPath($this->getHtml($url));
$text = fn(string $q) => trim($xpath->query($q)->item(0)?->textContent ?? '');
$name = $text("//h1[contains(@class,'product-title')]") ?: 'Sin nombre';
$sku = $text("//*[contains(@class,'sku')]");
$description = $text("//*[contains(@class,'product-description')]");
$priceRaw = $text("//*[contains(@class,'price')]//*[contains(@class,'value')]");
$imgNode = $xpath->query("//*[contains(@class,'product-gallery')]//img")->item(0);
$imageUrl = $imgNode ? $this->absolute($imgNode->getAttribute('src')) : '';
// limpiamos el precio de espacios y símbolos de divisa
$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' => $description,
'image_url' => $imageUrl,
'price_source' => $priceSource,
// conversión de divisas:
'price' => $this->converter->convert($priceSource),
'source_url' => $url,
];
}
}Paso 3. Importación a la base de datos de OpenCart
<?php
class OpenCartImporter
{
private PDO $pdo;
public function __construct(
array $dbConfig,
private string $prefix = 'oc_',
private int $storeId = 0,
private int $languageId = 1
) {
$dsn = "mysql:host={$dbConfig['host']};dbname={$dbConfig['database']};charset=utf8mb4";
$this->pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
}
private function seoKeyword(string $name): string
{
// generación simplificada de la URL amigable (sin acentos ni caracteres especiales)
$slug = mb_strtolower(trim($name));
$slug = strtr($slug, ['á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n']);
$slug = preg_replace('/[^a-z0-9\s-]/u', '', $slug);
$slug = preg_replace('/\s+/u', '-', $slug);
return $slug !== '' ? $slug : 'product';
}
public function importProduct(array $p, ?int $categoryId = null): int
{
$px = $this->prefix;
$now = date('Y-m-d H:i:s');
// 1) tabla principal del producto
$stmt = $this->pdo->prepare("
INSERT INTO {$px}product
(model, sku, quantity, price, image, status,
date_added, date_modified, date_available)
VALUES (:model, :sku, 100, :price, '', 1, :added, :modified, :available)
");
$stmt->execute([
':model' => $p['sku'] ?: mb_substr($p['name'], 0, 64),
':sku' => $p['sku'],
':price' => $p['price'],
':added' => $now,
':modified' => $now,
':available' => $now,
]);
$productId = (int)$this->pdo->lastInsertId();
// 2) descripción (nombre, texto, metadatos)
$this->pdo->prepare("
INSERT INTO {$px}product_description
(product_id, language_id, name, description, meta_title, meta_description)
VALUES (?, ?, ?, ?, ?, ?)
")->execute([
$productId, $this->languageId, $p['name'],
$p['description'], $p['name'], $p['name'],
]);
// 3) vínculo con la tienda
$this->pdo->prepare("
INSERT INTO {$px}product_to_store (product_id, store_id) VALUES (?, ?)
")->execute([$productId, $this->storeId]);
// 4) vínculo con la categoría
if ($categoryId !== null) {
$this->pdo->prepare("
INSERT INTO {$px}product_to_category (product_id, category_id) VALUES (?, ?)
")->execute([$productId, $categoryId]);
}
// 5) SEO URL (URL amigable)
$keyword = $this->seoKeyword($p['name']) . '-' . $productId;
// — OpenCart 3.x: tabla url_alias ---
$this->pdo->prepare("
INSERT INTO {$px}url_alias (query, keyword) VALUES (?, ?)
")->execute(["product_id={$productId}", $keyword]);
// — En OpenCart 4.x sería así (tabla seo_url):
// INSERT INTO {$px}seo_url (store_id, language_id, `key`, `value`, keyword)
// VALUES (:store, :lang, 'product_id', :pid, :keyword)
return $productId;
}
}Paso 4. Punto de entrada y ejecución por cron
<?php
require 'CurrencyConverter.php';
require 'SupplierParser.php';
require 'OpenCartImporter.php';
$converter = new CurrencyConverter('USD', 1.25);
$parser = new SupplierParser('https://supplier-example.com', $converter, 1.0);
// las credenciales pueden ir en un archivo aparte o tomarse del config.php de OpenCart
$importer = new OpenCartImporter([
'host' => 'localhost',
'user' => 'db_user',
'password' => 'db_password',
'database' => 'opencart_db',
], prefix: 'oc_', storeId: 0, languageId: 1);
$urls = $parser->parseCatalog('/catalog/category-1', maxPages: 3);
echo 'Productos encontrados: ' . count($urls) . PHP_EOL;
$imported = 0;
foreach ($urls as $url) {
try {
$product = $parser->parseProduct($url);
if ($product['price'] === null) {
echo "Omitido (sin precio): {$url}" . PHP_EOL;
continue;
}
$pid = $importer->importProduct($product, categoryId: 20);
$imported++;
printf("[%d] %s — %s USD -> %s € (ID %d)%s",
$imported, $product['name'], $product['price_source'], $product['price'], $pid, PHP_EOL);
} catch (Throwable $e) {
echo "Error en {$url}: {$e->getMessage()}" . PHP_EOL;
}
}
echo "Listo. Importados: {$imported}" . PHP_EOL;Programar la ejecución automática diaria es una línea en el crontab:
# cada día a las 4:00 sincronizamos el catálogo con el proveedor
0 4 * * * /usr/bin/php /var/www/opencart/parser/run.php >> /var/www/opencart/parser/parser.log 2>&1Las credenciales de la base pueden tomarse directamente de OpenCart:
require '/path/to/opencart/config.php';— tras esto quedan disponibles las constantesDB_HOSTNAME,DB_USERNAME,DB_PASSWORD,DB_DATABASEyDB_PREFIX, y no hará falta duplicar contraseñas en el scraper.
Todas las salvedades del apartado «fuera del ejemplo» (imágenes, deduplicación, atributos, caché, legalidad) siguen igual de vigentes para la versión en PHP.
Conclusión
Para una tienda estándar suele bastar la combinación «scraper listo + módulo de importación»: Export/Import Tool, los módulos comerciales del Marketplace o herramientas como Octoparse y Apify cubren la mayoría de los escenarios. Pero en cuanto aparece un sitio de origen atípico, una lógica de márgenes propia, la conversión de divisas con el tipo de cambio actual o la sincronización periódica de stock para una versión concreta de OpenCart, resulta más rentable y fiable encargar un scraper a medida.
¿Necesita scraping de tiendas online?
Si necesita poblar o sincronizar una tienda en OpenCart (o en cualquier otra plataforma), recopilar datos de los sitios de sus proveedores o configurar la actualización automática de precios con conversión de divisas, póngase en contacto con nosotros. Diseñaremos e implantaremos un scraper para su versión de OpenCart y sus tareas concretas.