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

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

Парсер под магазины на 1С-Битрикс: особенности редакций, готовые модули импорта и пример сбора каталога с конвертацией валюты.

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

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

В этой статье разберём, чем отличаются редакции «1С-Битрикс» с точки зрения парсинга, какие есть готовые решения, и напишем собственный парсер, который собирает данные с сайта поставщика и параллельно конвертирует валюту — причём задействуя встроенный в Битрикс модуль валют.


Почему редакция и архитектура Битрикса важны

В отличие от многих CMS, в Битриксе категорически не рекомендуется писать товары напрямую в базу: структура хранения сложная (инфоблоки, торговый каталог, цены, склады, торговые предложения), и прямой INSERT почти гарантированно сломает индексы и бизнес-логику. Правильный путь — работать через API Битрикса. Поэтому для парсера важна не столько «версия ядра», сколько редакция продукта и то, какие модули в ней доступны.

«1С-Битрикс: Управление сайтом» поставляется в пяти основных редакциях — «Старт», «Стандарт», «Малый бизнес», «Бизнес» и «Энтерпрайз». Полноценный интернет-магазин и нужные парсеру модули появляются начиная с «Малого бизнеса».

«Старт» и «Стандарт» — для контентных сайтов. Модуля «Торговый каталог» в базовой комплектации нет, полноценный магазин без доработок не собрать. Парсить сюда товары как товары каталога негде.

«Малый бизнес» — первая «магазинная» редакция. Содержит модули «Интернет-магазин» и «Торговый каталог», поддерживает интеграцию с «1С» и Яндекс.Маркет. Работает с одним типом цены и одним складом — этого достаточно для парсера среднего магазина.

«Бизнес» — расширенный магазин: несколько типов цен (опт/розница), многоскладовость, накопительные скидки, наборы и комплекты. Если вы парсите оптовые и розничные цены или раскладываете остатки по складам, парсер должен уметь это записывать — а значит, нужна именно «Бизнес».

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

Отдельно стоит «1С-Битрикс24: Интернет-магазин + CRM» — это другой продукт (магазин завязан на CRM-сущности и каталог CRM). Парсер под него пишется иначе, чем под «Управление сайтом», — учитывайте это, если у вас именно Битрикс24.

Ещё несколько вещей, которые парсер обязан понимать про Битрикс:

  • Инфоблоки (infoblocks) — товары хранятся как элементы инфоблока каталога, разделы инфоблока — это категории.
  • Торговые предложения (SKU / offers) — варианты товара (цвет, размер) лежат в отдельном связанном инфоблоке. Если у поставщика есть вариации, парсер должен создавать и сам товар, и его торговые предложения.
  • Модуль валют — в Битриксе есть встроенный модуль валют, который умеет автоматически подтягивать курсы ЦБ РФ по агенту. Это удобно: конвертацию можно делать средствами самого Битрикса, не изобретая свой источник курса.
  • CommerceML — стандартный XML-формат обмена с «1С». Через него же работает штатный импорт каталога.

Практический вывод: перед разработкой зафиксируйте редакцию, ID инфоблока каталога, схему свойств и то, используются ли торговые предложения и несколько типов цен. От этого зависит вся логика импорта.


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

Если магазин типовой, возможно, своё писать и не придётся.

Штатные средства Битрикса

В «Малом бизнесе» и «Бизнес» из коробки есть импорт каталога:

  • Импорт из CommerceML — основной канал обмена с «1С»: товары, цены, остатки, свойства. Если поставщик умеет отдавать CommerceML — это самый «родной» способ.
  • Импорт из CSV — «Торговый каталог → Экспорт/Импорт». Принимает подготовленный CSV с товарами, ценами и свойствами.

Сами по себе они не «ходят» по чужим сайтам — им нужен готовый файл. Поэтому их обычно ставят в пару к парсеру: парсер собирает CSV/XML, штатный импорт его заливает. Это самый безопасный для ядра вариант.

Модули из Маркетплейса 1С-Битрикс

Это расширения, которые и парсят, и грузят результат в инфоблоки прямо из админки:

  • «Загрузка товаров, парсер сайтов, импорт» (kitnet.import) — парсит сайты прямо из админки и добавляет товары в инфоблок: загрузка из всех категорий, автосоздание разделов и свойств, многопоточность, импорт CSV. Разработчики предлагают также подключение по API поставщика.
  • «Сотбит: Парсер контента» (shs.parser) — модуль от Sotbit: интеграция номенклатуры поставщиков с магазином из сайта-каталога, Excel, XML, YML, CSV; загрузка в инфоблоки и highload-блоки, работа через прокси, логирование, демо-режим с ограничениями.

Совет: у большинства таких модулей есть демо-режим (обычно несколько товаров / страниц). Обязательно протестируйте его на вашем конкретном доноре до покупки — далеко не все сайты парсятся «из коробки».

Внешние программы

  • Datacol — десктопная программа с десятками готовых настроек под популярные магазины. Собирает данные с сайтов, прайсов и маркетплейсов, обрабатывает (перевод, уникализация, наценка) и грузит в Битрикс полуавтоматически (через Excel + штатный импорт) или автоматически.

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


Пример 1: парсер на PHP через API Битрикса (с конвертацией валюты)

Поскольку Битрикс написан на PHP, самый правильный парсер — это PHP-скрипт, который подключает ядро Битрикса и пишет товары через его API. Конвертацию валюты сделаем средствами встроенного модуля валют, а наценку применим сами.

Код учебный. Перед запуском на боевом магазине сделайте резервную копию, убедитесь, что парсинг донора не нарушает его правил и закон, и протестируйте на копии.

Шаг 0. Подключение ядра и модулей

Скрипт запускается из консоли или по cron. Он бутстрапит Битрикс и подключает нужные модули.

php
<?php
// чтобы скрипт работал из консоли/крона, а не только из веб-контекста
$_SERVER['DOCUMENT_ROOT'] = '/var/www/bitrix';
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_NO_ACCELERATOR_RESET', true);

require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Bitrix\Main\Loader;

Loader::includeModule('iblock');    // инфоблоки (каталог)
Loader::includeModule('catalog');   // торговый каталог (цены, остатки)
Loader::includeModule('currency');  // модуль валют (курсы ЦБ)

Шаг 1. Конвертер валюты на встроенном модуле

Битрикс уже хранит и (по агенту) обновляет курсы ЦБ РФ. Берём курс из модуля валют через CCurrencyRates::ConvertCurrency, наценку добавляем отдельно.

php
<?php
class CurrencyConverter
{
    public function __construct(
        private string $from = 'USD',
        private string $to = 'RUB',
        private float $markup = 1.20   // наценка магазина: +20%
    ) {}

    /** Цена поставщика -> цена магазина по курсу Битрикса + наценка. */
    public function convert(?float $amount): ?float
    {
        if ($amount === null) {
            return null;
        }
        // ConvertCurrency сам берёт актуальный курс из модуля валют
        $converted = \CCurrencyRates::ConvertCurrency($amount, $this->from, $this->to);
        return round($converted * $this->markup, 2);
    }
}

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

Для разбора HTML используем встроенные DOMDocument + DOMXPath и cURL — без сторонних библиотек. Селекторы условные: под каждый донор их подбирают индивидуально.

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

    /** Ссылки на товары со страниц каталога с пагинацией. */
    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;
    }

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

Шаг 3. Импорт в каталог через API Битрикса

Здесь — главное отличие от других CMS: пишем не в базу напрямую, а через CIBlockElement (элемент инфоблока), CCatalogProduct (карточка торгового каталога) и CPrice (цена). Картинку скачиваем через CFile::MakeFileArray. Дубли проверяем по внешнему коду (XML_ID).

php
<?php
class BitrixImporter
{
    public function __construct(
        private int $iblockId,         // ID инфоблока каталога
        private int $sectionId,        // раздел (категория) по умолчанию
        private string $priceCurrency = 'RUB'
    ) {}

    /** Ищем товар по внешнему коду, чтобы не плодить дубли. */
    private function findByXmlId(string $xmlId): ?int
    {
        $res = \CIBlockElement::GetList(
            [],
            ['IBLOCK_ID' => $this->iblockId, 'XML_ID' => $xmlId],
            false, false, ['ID']
        );
        $row = $res->Fetch();
        return $row ? (int)$row['ID'] : null;
    }

    public function import(array $p): int
    {
        $el = new \CIBlockElement();
        $xmlId = $p['sku'] !== '' ? $p['sku'] : md5($p['source_url']);

        $fields = [
            'IBLOCK_ID'         => $this->iblockId,
            'IBLOCK_SECTION_ID' => $this->sectionId,
            'NAME'              => $p['name'],
            'XML_ID'            => $xmlId,                 // внешний код = артикул донора
            'DETAIL_TEXT'       => $p['description'],
            'DETAIL_TEXT_TYPE'  => 'html',
            'ACTIVE'            => 'Y',
        ];

        // картинку скачиваем и привязываем (если донор отдаёт)
        if ($p['image_url'] !== '') {
            $file = \CFile::MakeFileArray($p['image_url']);
            if ($file) {
                $fields['DETAIL_PICTURE'] = $file;
            }
        }

        $existingId = $this->findByXmlId($xmlId);
        if ($existingId) {
            // товар уже есть — обновляем (без перезаписи картинки)
            unset($fields['DETAIL_PICTURE'], $fields['XML_ID']);
            $el->Update($existingId, $fields);
            $productId = $existingId;
        } else {
            $productId = $el->Add($fields);
            if (!$productId) {
                throw new RuntimeException('CIBlockElement: ' . $el->LAST_ERROR);
            }
        }

        // карточка торгового каталога (остаток)
        \CCatalogProduct::Add([
            'ID'                => $productId,
            'QUANTITY'          => 100,
            'AVAILABLE'         => 'Y',
        ]);

        // базовая цена (уже сконвертированная и с наценкой)
        if ($p['price'] !== null) {
            \CPrice::SetBasePrice($productId, $p['price'], $this->priceCurrency);
        }

        return (int)$productId;
    }
}

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

php
<?php
// после блока подключения ядра (Шаг 0) и классов выше:

$converter = new CurrencyConverter('USD', 'RUB', 1.25);
$parser    = new SupplierParser('https://supplier-example.com', $converter, 1.0);
$importer  = new BitrixImporter(iblockId: 12, sectionId: 47, priceCurrency: 'RUB');

$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;
        }
        $id = $importer->import($product);
        $imported++;
        printf("[%d] %s — %s USD -> %s ₽  (ID %d)%s",
            $imported, $product['name'], $product['price_source'], $product['price'], $id, 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/bitrix/local/parser/run.php >> /var/www/bitrix/local/parser/parser.log 2>&1

Альтернатива cron — агент Битрикса (CAgent::AddAgent): он запускается на хитах сайта и удобен, когда нет доступа к системному планировщику. Но для тяжёлого парсинга надёжнее именно cron, чтобы не нагружать посетителей.


Пример 2: сбор на Python → CSV → штатный импорт

Если не хочется подключать ядро и трогать боевой код, парсинг можно развязать с импортом: собрать данные любым удобным инструментом (например, на Python), сложить в CSV, а заливку доверить штатному импорту «Торговый каталог → Экспорт/Импорт». Так парсер вообще не зависит от Битрикса и его обновлений.

Здесь конвертацию валюты делаем сами — берём курс ЦБ РФ и применяем наценку (так же, как в PHP-варианте, но на стороне сборщика).

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 CbrRate:
    """Курс ЦБ РФ с кешем на день."""
    URL = "https://www.cbr.ru/scripts/XML_daily.asp"

    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)
            r.encoding = "windows-1251"
            tree = ET.fromstring(r.text)
            for v in tree.findall("Valute"):
                if v.find("CharCode").text == self.currency:
                    nominal = int(v.find("Nominal").text)
                    value = float(v.find("Value").text.replace(",", "."))
                    self._rate = value / nominal
                    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):
    cbr = CbrRate(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

            # конвертация валюты: курс ЦБ * наценка
            price_rub = round(price_src * cbr.rate() * markup, 2) if price_src else ""

            rows.append({
                "XML_ID": sku.text.strip() if sku else url,
                "NAME": name.text.strip() if name else "Без названия",
                "DETAIL_TEXT": descr.decode_contents().strip() if descr else "",
                "PRICE": price_rub,
                "CURRENCY": "RUB",
                "PICTURE": urljoin(base_url, img["src"]) if img else "",
            })
            time.sleep(delay)
        time.sleep(delay)

    # CSV под штатный импорт Торгового каталога (разделитель ;)
    with open(out_csv, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=rows[0].keys(), delimiter=";")
        writer.writeheader()
        writer.writerows(rows)

    print(f"Готово. Записано строк: {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",
    )

Дальше CSV загружается в админке: Магазин → Настройки → Настройки модулей → Торговый каталог → Экспорт/Импорт → Импорт CSV, где колонки сопоставляются с полями товара (внешний код, название, описание, цена, валюта, картинка). Плюс подхода — парсер не зависит от ядра Битрикса; минус — заливка не «на лету», а отдельным шагом.


Что важно в реальном проекте

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

  • Торговые предложения (SKU) — если у товара есть вариации, создавать элементы в инфоблоке предложений и привязывать их к товару, а цену/остаток вешать на предложения, а не на товар.
  • Свойства каталога — сопоставлять характеристики донора со свойствами инфоблока (CIBlockElement::SetPropertyValues), при необходимости автосоздавать значения справочников.
  • Разделы — автосоздание дерева категорий по структуре донора через CIBlockSection.
  • Несколько типов цен и складов — для редакции «Бизнес»/«Энтерпрайз»: оптовые/розничные цены, остатки по складам.
  • Дедупликацию и синхронизацию — обновлять цену/остаток по XML_ID, а не плодить дубли; так разовый парсинг превращается в регулярную синхронизацию.
  • Курс и наценку — хранить в конфиге и логировать, по какому курсу пересчитана партия, чтобы цены были воспроизводимы.
  • Этичность и легальность — соблюдать robots.txt, делать паузы между запросами и учитывать условия использования сайта-донора.

Итог

Для типового магазина часто хватает связки «парсер собирает файл + штатный импорт CommerceML/CSV» или готового модуля из Маркетплейса (kitnet.import, «Сотбит: Парсер контента», Datacol). Но как только появляется нестандартный донор, своя схема свойств и торговых предложений, несколько типов цен, конвертация валют по актуальному курсу или регулярная синхронизация остатков под вашу редакцию Битрикса — надёжнее заказать парсер под задачу, написанный через API Битрикса.


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

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