CMS и платформы 17 мин чтения

Парсер для OpenCart: версии, готовые решения и пример с конвертацией валюты

Парсер для OpenCart: отличия версий, готовые модули наполнения каталога и пример импорта товаров с пересчётом цен в нужную валюту.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 20 февраля 2025

Наполнение интернет-магазина вручную — это самая дорогая и скучная часть запуска. Когда у поставщика тысячи позиций, а цены меняются каждую неделю, без автоматизации не обойтись. Здесь на сцену выходит парсер для OpenCart — программа, которая собирает данные о товарах с сайта-донора (поставщика, дистрибьютора, маркетплейса) и заливает их в ваш магазин: с названиями, описаниями, фотографиями, характеристиками и, что особенно важно, с правильно пересчитанными ценами.

В этой статье разберём, чем отличаются версии OpenCart с точки зрения парсинга, какие есть готовые решения, и напишем собственный парсер на Python, который собирает товары с сайта поставщика и параллельно конвертирует валюту.


Почему версия OpenCart имеет значение

Парсер в конечном счёте пишет данные в базу или формирует файл импорта, а структура базы и логика SEO-ссылок у разных версий OpenCart различаются. Игнорировать это — гарантированно получить «битые» товары или ошибки импорта.

OpenCart появился ещё в 2005 году и с тех пор прошёл несколько крупных переработок: версия 1.5 (2011) принесла новый редактор макетов и админку, ветка 2.x (2014) — модульную архитектуру и улучшенную систему расширений, версия 3.0 (2017) — мультимагазинность и Marketplace, а ветка 4.x стала актуальной с обновлённой кодовой базой и возвращением OCMOD. Каждая из этих веток до сих пор встречается в живых проектах.

Вот что важно учитывать парсеру:

OpenCart 1.5.x — устаревшая, но всё ещё встречается ветка. Простая структура таблиц, но другие имена полей в ряде таблиц. Готовые модули импорта под неё почти не поддерживаются.

OpenCart 2.x — самая распространённая «старая» рабочая ветка. Здесь живут многие из проверенных модулей импорта. SEO-ссылки хранятся в таблице url_alias.

OpenCart 3.x — наиболее популярная версия для боевых магазинов на момент написания. Структура близка к 2.x, SEO-ссылки — также в url_alias. Большинство готовых парсеров и модулей рассчитаны именно на 2.x–3.x.

OpenCart 4.x — актуальная ветка. Здесь принципиальное изменение: SEO-ссылки переехали из url_alias в таблицу seo_url (с привязкой к store_id и language_id), изменилась и часть контроллеров. Парсер, написанный под OC3, без правок в OC4 не положит корректные ЧПУ.

ocStore / LiveStore — популярные в рунете русифицированные сборки на базе OpenCart. Структура данных совпадает с соответствующей версией OpenCart, но «коробочные» модули иногда ведут себя иначе, поэтому их всегда стоит тестировать отдельно.

Практический вывод: прежде чем писать или покупать парсер, зафиксируйте точную версию магазина (она видна в админке внизу страницы или в index.php ядра) и префикс таблиц в базе (по умолчанию oc_, но часто меняют на свой).


Готовые решения: что есть на рынке

Если бюджет ограничен, а магазин типовой — возможно, писать свой парсер и не нужно. Готовых инструментов хватает, и они делятся на несколько категорий.

Модули импорта внутри OpenCart

Это расширения, которые сами по себе не «ходят» по чужим сайтам, но принимают подготовленный файл (CSV / XLS / XLSX / YML / XML) и грузят его в каталог:

  • Export/Import Tool — бесплатный и популярный модуль, справляется с импортом до нескольких тысяч товаров (ограничения зависят от хостинга). Хороший вариант для старта.
  • CSV Price Pro import/export — платный модуль (около 1950 ₽ за домен), более гибкий по части настройки соответствия полей.
  • anyCSV/XLS/YML PRO — мощный коммерческий модуль для импорта CSV, XLS, XLSX, YML, CML под OC 2–3.
  • ScrapeSmart Catalog Import — модуль для массового импорта/обновления каталога через AJAX-очередь (OpenCart, ocStore, LiveStore 3).

Связка «парсер собирает файл → модуль импорта его заливает» — самая надёжная: вы видите данные глазами перед загрузкой и ничего не ломаете в базе напрямую.

Комплексные парсеры-модули

Это решения «всё в одном» — они и парсят сайты-доноры, и грузят результат в магазин:

  • Price Master — комплексный модуль с встроенным многопоточным парсером сайтов, переводчиком, AI-генераторами текста, планировщиком и гибким сопоставлением данных. Лицензия выдаётся на год на один домен.
  • «Поставщики АОП» — один из самых известных в рунете модулей парсинга и импорта: создаёт категории, грузит тысячи товаров с описаниями, опциями, атрибутами и фото, умеет стандартизировать атрибуты «на лету».

Внешние программы и облачные сервисы

  • Datacol — десктопная программа с десятками готовых настроек под популярные магазины; собирает данные с прайсов, сайтов и маркетплейсов и выгружает их в OpenCart (полуавтоматически через файл или напрямую в базу).
  • ParseMX — гибкий парсер для синхронизации OpenCart с сайтами поставщиков: копирует структуру категорий, переводит тексты, поддерживает все поля товара (включая h1, title, SEO URL), работает с прокси и обновляет цены/остатки.

Когда готовое решение не подходит? Когда у поставщика нетривиальная вёрстка или защита от парсинга, нужна специфическая логика наценки, конвертация валют по своему курсу, привязка к своей структуре категорий или регулярная синхронизация остатков. В этих случаях пишут парсер под задачу — к нему и переходим.


Пример: свой парсер с импортом и конвертацией валюты

Разберём учебный парсер на Python, который:

  1. обходит каталог поставщика и собирает карточки товаров;
  2. вытаскивает название, цену, описание, артикул и картинку;
  3. конвертирует цену из валюты поставщика в валюту магазина по актуальному курсу с заданной наценкой;
  4. импортирует товары прямо в базу OpenCart.

Код приведён в учебных целях. Перед запуском на боевом магазине обязательно сделайте бэкап базы, проверьте, что парсинг сайта-донора не нарушает его правил и закон, и протестируйте всё на копии магазина.

Стек и подготовка

bash
pip install requests beautifulsoup4 pymysql lxml
  • requests + beautifulsoup4 — загрузка и разбор HTML;
  • pymysql — запись в базу OpenCart;
  • курс валют берём из публичного API (в примере — ЦБ РФ, можно заменить на любой другой).

Шаг 1. Конвертер валюты

Сначала отдельный модуль, который получает курс и пересчитывает цену с наценкой. Курс кешируется, чтобы не дёргать API на каждый товар.

python
import requests
import xml.etree.ElementTree as ET
from datetime import date

class CurrencyConverter:
    """Получает курс ЦБ РФ и пересчитывает цену поставщика в валюту магазина."""

    CBR_URL = "https://www.cbr.ru/scripts/XML_daily.asp"

    def __init__(self, source_currency="USD", markup=1.20):
        # markup=1.20 — наценка магазина +20% поверх курса
        self.source_currency = source_currency
        self.markup = markup
        self._rate = None
        self._rate_date = None

    def _fetch_rate(self):
        """Тянет курс нужной валюты к рублю из XML ЦБ РФ."""
        resp = requests.get(self.CBR_URL, timeout=15)
        resp.encoding = "windows-1251"
        tree = ET.fromstring(resp.text)

        for valute in tree.findall("Valute"):
            if valute.find("CharCode").text == self.source_currency:
                nominal = int(valute.find("Nominal").text)
                value = float(valute.find("Value").text.replace(",", "."))
                return value / nominal  # курс за 1 единицу валюты

        raise ValueError(f"Валюта {self.source_currency} не найдена в ответе ЦБ")

    @property
    def rate(self):
        # кешируем курс на текущую дату
        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):
        """Цену поставщика -> цена магазина (в рублях) с наценкой."""
        if amount is None:
            return None
        price = float(amount) * self.rate * self.markup
        return round(price, 2)

Шаг 2. Парсер карточек поставщика

Селекторы (.product-card, .price и т.д.) — условные: под каждый сайт-донор их подбирают индивидуально, открыв исходный код страницы.

python
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  # пауза между запросами, чтобы не «ддосить» донора
        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):
        """Собирает ссылки на товары со страниц каталога с пагинацией."""
        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  # страницы кончились
            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):
        """Разбирает одну карточку товара."""
        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")

        # цену чистим от пробелов и символов валюты
        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 "Без названия",
            "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,
            # вот здесь и происходит конвертация валюты:
            "price": self.converter.convert(price_source),
            "source_url": url,
        }

Шаг 3. Импорт в базу OpenCart

Здесь учитываем то, о чём говорили в начале: структура зависит от версии. Пример рассчитан на OpenCart 3.x; ключевое отличие для OC4 вынесено в комментарий.

python
import pymysql
from datetime import datetime
from transliterate import slugify  # pip install transliterate

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, "ru") 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) основная таблица товара
        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) описание (название, текст, мета) — на каждый язык
        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) привязка к магазину
        cur.execute(f"""
            INSERT INTO {px}product_to_store (product_id, store_id)
            VALUES (%s, %s)
        """, (product_id, self.store_id))

        # 4) привязка к категории (если задана)
        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 (ЧПУ)
        keyword = f"{self._seo_keyword(p['name'])}-{product_id}"
        # --- OpenCart 3.x: таблица url_alias ---
        cur.execute(f"""
            INSERT INTO {px}url_alias (query, keyword)
            VALUES (%s, %s)
        """, (f"product_id={product_id}", keyword))
        # --- OpenCart 4.x было бы так (таблица 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()

Шаг 4. Собираем всё вместе

python
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"Найдено товаров: {len(product_urls)}")

    imported = 0
    for url in product_urls:
        try:
            product = parser.parse_product(url)
            if product["price"] is None:
                print(f"Пропуск (нет цены): {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"Ошибка на {url}: {e}")

    importer.close()
    print(f"Готово. Импортировано: {imported}")

if __name__ == "__main__":
    main()

Что осталось «за кадром» и важно в реальном проекте

Учебный пример намеренно упрощён. В боевом парсере дополнительно нужно предусмотреть:

  • Загрузку и привязку изображений — скачать фото в image/catalog/... и записать путь в product.image и oc_product_image. Часто сайты-доноры отдают 403 при прямом скачивании, и нужен корректный реферер/заголовки.
  • Дедупликацию — проверять по sku/model, есть ли товар уже в базе, и обновлять цену/остаток, а не плодить дубли. Это же превращает разовый парсинг в регулярную синхронизацию.
  • Атрибуты и опцииoc_product_attribute, oc_product_option со своей структурой связей.
  • Категории — автосоздание дерева категорий по структуре донора (oc_category, oc_category_description, oc_category_path).
  • Очистку кеша OpenCart и пересборку SEO-индексов после массового импорта.
  • Курс и наценку — хранить их в конфиге и логировать, по какому курсу пересчитана каждая партия, чтобы цены были воспроизводимы и прозрачны.
  • Этичность и легальность — соблюдать robots.txt, разумные паузы между запросами и условия использования сайта-донора.

Тот же парсер на PHP — прямо на сервере магазина

Python удобен, но требует отдельного окружения. А OpenCart сам написан на PHP и работает на MySQL — значит, парсер можно сделать на том же стеке и запускать прямо на хостинге магазина, без установки Python и сторонних рантаймов. Плюсы такого подхода:

  • ничего не нужно ставить дополнительно — PHP уже есть на сервере;
  • можно переиспользовать config.php OpenCart, чтобы не дублировать доступы к базе;
  • легко повесить на cron для регулярной синхронизации;
  • при желании скрипт можно подключить к ядру OpenCart и писать товары через его модели, а не напрямую в таблицы.

Для разбора HTML используем встроенные DOMDocument + DOMXPath (без сторонних библиотек), для запросов — cURL, для базы — PDO.

Шаг 1. Конвертер валюты

php
<?php
class CurrencyConverter
{
    private const CBR_URL = 'https://www.cbr.ru/scripts/XML_daily.asp';

    private ?float $rate = null;
    private ?string $rateDate = null;

    public function __construct(
        private string $sourceCurrency = 'USD',
        private float $markup = 1.20  // наценка магазина: +20% поверх курса
    ) {}

    private function fetchRate(): float
    {
        $xmlRaw = file_get_contents(self::CBR_URL);
        // ответ ЦБ в windows-1251 — приводим к UTF-8
        $xmlRaw = mb_convert_encoding($xmlRaw, 'UTF-8', 'windows-1251');
        $xmlRaw = preg_replace('/encoding="windows-1251"/', 'encoding="UTF-8"', $xmlRaw);

        $xml = new SimpleXMLElement($xmlRaw);
        foreach ($xml->Valute as $valute) {
            if ((string)$valute->CharCode === $this->sourceCurrency) {
                $nominal = (int)$valute->Nominal;
                $value = (float)str_replace(',', '.', (string)$valute->Value);
                return $value / $nominal; // курс за 1 единицу валюты
            }
        }
        throw new RuntimeException("Валюта {$this->sourceCurrency} не найдена в ответе ЦБ");
    }

    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);
    }
}

Шаг 2. Парсер карточек поставщика

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 makeXPath(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, '/');
    }

    /** Собирает ссылки на товары со страниц каталога с пагинацией. */
    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; // страницы кончились
            }
            foreach ($links as $a) {
                $urls[] = $this->absolute($a->getAttribute('href'));
            }
            usleep((int)($this->delay * 1_000_000));
        }
        return $urls;
    }

    /** Разбирает одну карточку товара. */
    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')]") ?: 'Без названия';
        $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')) : '';

        // чистим цену от пробелов и валютных символов
        $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,
            // конвертация валюты:
            'price'        => $this->converter->convert($priceSource),
            'source_url'   => $url,
        ];
    }
}

Шаг 3. Импорт в базу OpenCart

php
<?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
    {
        // упрощённая транслитерация кириллицы в ЧПУ
        $slug = mb_strtolower(trim($name));
        $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) основная таблица товара
        $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) описание (название, текст, мета)
        $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) привязка к магазину
        $this->pdo->prepare("
            INSERT INTO {$px}product_to_store (product_id, store_id) VALUES (?, ?)
        ")->execute([$productId, $this->storeId]);

        // 4) привязка к категории
        if ($categoryId !== null) {
            $this->pdo->prepare("
                INSERT INTO {$px}product_to_category (product_id, category_id) VALUES (?, ?)
            ")->execute([$productId, $categoryId]);
        }

        // 5) SEO URL (ЧПУ)
        $keyword = $this->seoKeyword($p['name']) . '-' . $productId;
        // --- OpenCart 3.x: таблица url_alias ---
        $this->pdo->prepare("
            INSERT INTO {$px}url_alias (query, keyword) VALUES (?, ?)
        ")->execute(["product_id={$productId}", $keyword]);
        // --- OpenCart 4.x было бы так (таблица seo_url):
        // INSERT INTO {$px}seo_url (store_id, language_id, `key`, `value`, keyword)
        //   VALUES (:store, :lang, 'product_id', :pid, :keyword)

        return $productId;
    }
}

Шаг 4. Точка входа и запуск по cron

php
<?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);

// доступы можно вынести в конфиг или подтянуть из config.php 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 'Найдено товаров: ' . count($urls) . PHP_EOL;

$imported = 0;
foreach ($urls as $url) {
    try {
        $product = $parser->parseProduct($url);
        if ($product['price'] === null) {
            echo "Пропуск (нет цены): {$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 "Ошибка на {$url}: {$e->getMessage()}" . PHP_EOL;
    }
}

echo "Готово. Импортировано: {$imported}" . PHP_EOL;

Поставить на ежедневный автозапуск — одна строка в crontab:

bash
# каждый день в 4:00 синхронизируем каталог с поставщиком
0 4 * * * /usr/bin/php /var/www/opencart/parser/run.php >> /var/www/opencart/parser/parser.log 2>&1

Подтянуть доступы к базе прямо из OpenCart можно так: require '/path/to/opencart/config.php'; — после этого станут доступны константы DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_DATABASE и DB_PREFIX, и дублировать пароли в парсере не придётся.

Все оговорки из раздела «за кадром» (изображения, дедупликация, атрибуты, кеш, легальность) для PHP-версии так же актуальны.


Итог

Для типового магазина часто достаточно связки «готовый парсер + модуль импорта» — Export/Import Tool, CSV Price Pro, Price Master, Datacol или ParseMX закроют большинство сценариев. Но как только появляется нестандартный донор, своя логика наценки, конвертация валют по актуальному курсу или регулярная синхронизация остатков под конкретную версию OpenCart — выгоднее и надёжнее заказать парсер под задачу.


Нужен парсинг интернет-магазинов?

Если вам нужно наполнить или синхронизировать магазин на OpenCart (или любой другой платформе), собрать данные с сайтов поставщиков, настроить автоматическое обновление цен с конвертацией валют — обращайтесь к нам. Мы спроектируем и внедрим парсер под вашу версию OpenCart и ваши задачи.