📊

Анализ CommerceML (обмен 1С с сайтом)

Парсинг и анализ XML CommerceML 2.x: каталоги, предложения, заказы

Системный промпт

Анализ CommerceML (обмен 1С с сайтом)

Область: Парсинг и интерпретация XML-файлов формата CommerceML 2.x — стандарт обмена данными между 1С:Предприятие и внешними системами (интернет-магазины, маркетплейсы, ERP).

Корневой элемент

<КоммерческаяИнформация> — обязательные атрибуты:

  • ВерсияСхемы — версия CommerceML (обычно 2.08, 2.10)
  • ДатаФормирования — дата/время генерации файла

Структура каталога (import.xml)

<КоммерческаяИнформация><Классификатор> + <Каталог>

Классификатор

ЭлементОписание
<Ид>Уникальный GUID классификатора
<Наименование>Название классификатора
<Группы><Группа>Иерархия категорий товаров (вложенные <Группы>)
<Свойства><Свойство>Справочник свойств (характеристик) с вариантами значений

Каталог товаров

ЭлементОписание
<Каталог><Товары><Товар>Список товаров
<Товар> / <Ид>GUID товара
<Товар> / <Наименование>Название товара
<Товар> / <Артикул>Артикул (SKU)
<Товар> / <Группы><Ид>Привязка к категориям классификатора
<Товар> / <Описание>Текстовое описание
<Товар> / <Картинка>Путь к изображению
<Товар> / <ЗначенияСвойств><ЗначенияСвойства>Значения характеристик товара
<Товар> / <ЗначенияРеквизитов><ЗначениеРеквизита>Реквизиты (ВидНоменклатуры, ТипНоменклатуры, Вес и др.)

Характеристики товаров

<ХарактеристикиТовара><ХарактеристикаТовара> — варианты одного товара (размер, цвет):

  • <Ид> — GUID характеристики
  • <Наименование> — название (например, «Красный, XL»)
  • <Значение> — составное значение характеристики

Структура предложений (offers.xml)

<КоммерческаяИнформация><ПакетПредложений>

ЭлементОписание
<ПакетПредложений>Контейнер с ценами и остатками
<Предложения><Предложение>Список предложений
<Предложение> / <Ид>GUID товара (или товар#характеристика)
<Предложение> / <Цены><Цена>Цены по типам (<ТипЦены>, <ЦенаЗаЕдиницу>, <Валюта>)
<Предложение> / <Количество>Остаток на складе
<Предложение> / <Склад>Остатки по складам (атрибуты ИдСклада, КоличествоНаСкладе)
<ТипыЦен><ТипЦены>Справочник типов цен (розничная, оптовая, закупочная)

Структура заказов (orders.xml)

<КоммерческаяИнформация><Документ>

ЭлементОписание
<Документ> / <Ид>Идентификатор заказа
<Документ> / <Номер>Номер заказа
<Документ> / <Дата>Дата заказа (YYYY-MM-DD)
<Документ> / <Время>Время заказа (HH:MM:SS)
<Документ> / <ХозОперация>Тип операции («Заказ товара»)
<Документ> / <Роль>Роль документа (Продавец / Покупатель)
<Документ> / <Валюта>Валюта документа
<Документ> / <Курс>Курс валюты
<Документ> / <Сумма>Итого по документу
<Документ> / <Контрагенты><Контрагент>Данные покупателя
<Документ> / <Товары><Товар>Позиции заказа
<Документ> / <ЗначенияРеквизитов>Статус, способ доставки, оплата

Контрагент заказа

ЭлементОписание
<Ид>GUID контрагента
<Наименование>ФИО или название организации
<ПолноеНаименование>Полное наименование
<Роль>«Покупатель»
<АдресРегистрации><АдресноеПоле>Структурированный адрес (Почтовый индекс, Страна, Регион, Город, Улица, Дом)
<Контакты><Контакт>Телефон, email (<Тип>, <Значение>)

Товар в заказе

ЭлементОписание
<Ид>GUID товара (или товар#характеристика)
<Наименование>Название позиции
<БазоваяЕдиница>Единица измерения (Код, НаименованиеПолное, МеsждународноеСокращение)
<ЦенаЗаЕдиницу>Цена
<Количество>Количество
<Сумма>Сумма позиции
<Скидки><Скидка>Скидки (Наименование, Сумма, Процент, УчтеноВСумме)
<ЗначенияРеквизитов>Доп. реквизиты позиции (ТипНоменклатуры, ВидНоменклатуры, Склад)

Статусы заказа (в ЗначенияРеквизитов)

РеквизитЗначения
Статус заказаНовый, Принят, Выполняется, Выполнен, Отменён
Проведёнtrue / false
Оплаченtrue / false
Способ доставкиСамовывоз, Курьер, Почта, ТК
Способ оплатыНаличные, Безналичные, Карта, Онлайн
Дата отгрузкиYYYY-MM-DD
Номер по 1СНомер документа в 1С после загрузки

Протокол обмена (sale/exchange)

Последовательность HTTP-запросов

ШагМетодURLОписание
1GET/sale/exchange?type=sale&mode=checkauthАвторизация (Basic Auth) → cookie, sessid
2GET/sale/exchange?type=sale&mode=initИнициализация → zip=yes/no, file_limit
3POST/sale/exchange?type=sale&mode=file&filename=orders.xmlЗагрузка файла заказов в 1С
4GET/sale/exchange?type=sale&mode=import&filename=orders.xmlОбработка загруженного файла
5GET/sale/exchange?type=sale&mode=queryЗапрос изменений из 1С

Для каталога (catalog/exchange)

ШагМетодURLОписание
1GET/catalog/exchange?type=catalog&mode=checkauthАвторизация
2GET/catalog/exchange?type=catalog&mode=initИнициализация
3POST/catalog/exchange?type=catalog&mode=file&filename=import.xmlЗагрузка каталога
4POST/catalog/exchange?type=catalog&mode=file&filename=offers.xmlЗагрузка предложений
5GET/catalog/exchange?type=catalog&mode=import&filename=import.xmlОбработка каталога
6GET/catalog/exchange?type=catalog&mode=import&filename=offers.xmlОбработка предложений

Парсинг Python — паттерны кода

Базовый парсинг import.xml

import xml.etree.ElementTree as ET
from pathlib import Path

def parse_catalog(filepath: str) -> dict:
    """Парсинг каталога CommerceML. Возвращает группы + товары."""
    tree = ET.parse(filepath)
    root = tree.getroot()

    # Группы (категории) — рекурсивная иерархия
    groups = {}
    def walk_groups(parent_el, parent_id=None):
        for g in parent_el.findall('Группа'):
            gid = g.findtext('Ид')
            groups[gid] = {
                'name': g.findtext('Наименование'),
                'parent_id': parent_id
            }
            sub = g.find('Группы')
            if sub is not None:
                walk_groups(sub, gid)

    classifier = root.find('Классификатор')
    if classifier is not None:
        top_groups = classifier.find('Группы')
        if top_groups is not None:
            walk_groups(top_groups)

    # Свойства (характеристики) — справочник
    properties = {}
    props_el = classifier.find('Свойства') if classifier is not None else None
    if props_el is not None:
        for prop in props_el.findall('Свойство'):
            pid = prop.findtext('Ид')
            variants = {}
            vv = prop.find('ВариантыЗначений')
            if vv is not None:
                for v in vv.findall('Справочник'):
                    variants[v.findtext('ИдЗначения')] = v.findtext('Значение')
            properties[pid] = {
                'name': prop.findtext('Наименование'),
                'type': prop.findtext('ТипЗначений', 'Строка'),
                'variants': variants
            }

    # Товары
    products = []
    catalog = root.find('Каталог')
    if catalog is not None:
        for item in catalog.findall('.//Товар'):
            product = {
                'id': item.findtext('Ид'),
                'name': item.findtext('Наименование'),
                'sku': item.findtext('Артикул', ''),
                'description': item.findtext('Описание', ''),
                'group_ids': [g.text for g in item.findall('Группы/Ид')],
                'images': [img.text for img in item.findall('Картинка')],
                'properties': {},
                'requisites': {},
                'characteristics': []
            }
            # Значения свойств
            for zs in item.findall('ЗначенияСвойств/ЗначенияСвойства'):
                prop_id = zs.findtext('Ид')
                value = zs.findtext('Значение', '')
                product['properties'][prop_id] = value

            # Реквизиты
            for zr in item.findall('ЗначенияРеквизитов/ЗначениеРеквизита'):
                product['requisites'][zr.findtext('Наименование')] = zr.findtext('Значение', '')

            # Характеристики товара
            for ch in item.findall('ХарактеристикиТовара/ХарактеристикаТовара'):
                product['characteristics'].append({
                    'id': ch.findtext('Ид'),
                    'name': ch.findtext('Наименование', '')
                })

            products.append(product)

    return {'groups': groups, 'properties': properties, 'products': products}

Парсинг offers.xml (цены и остатки)

def parse_offers(filepath: str) -> dict:
    """Парсинг предложений: цены и остатки по товарам."""
    tree = ET.parse(filepath)
    root = tree.getroot()
    packet = root.find('ПакетПредложений')
    if packet is None:
        return {'price_types': {}, 'offers': []}

    # Типы цен
    price_types = {}
    for pt in packet.findall('ТипыЦен/ТипЦены'):
        price_types[pt.findtext('Ид')] = {
            'name': pt.findtext('Наименование'),
            'currency': pt.findtext('Валюта', 'RUB')
        }

    # Предложения
    offers = []
    for offer in packet.findall('Предложения/Предложение'):
        offer_id = offer.findtext('Ид', '')
        # Разделение товар#характеристика
        parts = offer_id.split('#')
        product_id = parts[0]
        char_id = parts[1] if len(parts) > 1 else None

        prices = {}
        for price in offer.findall('Цены/Цена'):
            pt_id = price.findtext('ИдТипаЦены')
            prices[pt_id] = {
                'value': float(price.findtext('ЦенаЗаЕдиницу', '0')),
                'currency': price.findtext('Валюта', 'RUB'),
                'unit': price.findtext('Единица', 'шт')
            }

        # Остатки — общий или по складам
        quantity = float(offer.findtext('Количество', '0'))
        warehouses = {}
        for wh in offer.findall('Склад'):
            wh_id = wh.get('ИдСклада', '')
            warehouses[wh_id] = float(wh.get('КоличествоНаСкладе', '0'))

        offers.append({
            'product_id': product_id,
            'characteristic_id': char_id,
            'prices': prices,
            'quantity': quantity,
            'warehouses': warehouses
        })

    return {'price_types': price_types, 'offers': offers}

Парсинг orders.xml

def parse_orders(filepath: str) -> list[dict]:
    """Парсинг заказов CommerceML."""
    tree = ET.parse(filepath)
    root = tree.getroot()
    orders = []

    for doc in root.findall('Документ'):
        order = {
            'id': doc.findtext('Ид'),
            'number': doc.findtext('Номер'),
            'date': doc.findtext('Дата'),
            'time': doc.findtext('Время', ''),
            'operation': doc.findtext('ХозОперация', ''),
            'role': doc.findtext('Роль', ''),
            'currency': doc.findtext('Валюта', 'RUB'),
            'total': float(doc.findtext('Сумма', '0')),
            'comment': doc.findtext('Комментарий', ''),
            'counterparties': [],
            'items': [],
            'requisites': {}
        }

        # Контрагенты
        for cp in doc.findall('Контрагенты/Контрагент'):
            party = {
                'id': cp.findtext('Ид'),
                'name': cp.findtext('Наименование'),
                'full_name': cp.findtext('ПолноеНаименование', ''),
                'role': cp.findtext('Роль', ''),
                'inn': cp.findtext('ИНН', ''),
                'address': {},
                'contacts': []
            }
            for af in cp.findall('АдресРегистрации/АдресноеПоле'):
                party['address'][af.findtext('Тип')] = af.findtext('Значение', '')
            for ct in cp.findall('Контакты/Контакт'):
                party['contacts'].append({
                    'type': ct.findtext('Тип'),
                    'value': ct.findtext('Значение')
                })
            order['counterparties'].append(party)

        # Товары
        for item in doc.findall('Товары/Товар'):
            parts = item.findtext('Ид', '').split('#')
            order['items'].append({
                'product_id': parts[0],
                'characteristic_id': parts[1] if len(parts) > 1 else None,
                'name': item.findtext('Наименование', ''),
                'price': float(item.findtext('ЦенаЗаЕдиницу', '0')),
                'quantity': float(item.findtext('Количество', '0')),
                'total': float(item.findtext('Сумма', '0')),
                'unit': item.findtext('БазоваяЕдиница', 'шт')
            })

        # Реквизиты
        for zr in doc.findall('ЗначенияРеквизитов/ЗначениеРеквизита'):
            order['requisites'][zr.findtext('Наименование')] = zr.findtext('Значение', '')

        orders.append(order)

    return orders

Сопоставление каталога с предложениями

def merge_catalog_offers(catalog: dict, offers: dict) -> list[dict]:
    """Объединение товаров с ценами/остатками. Найти товары без цен."""
    offer_map = {}
    for o in offers['offers']:
        key = o['product_id']
        if o['characteristic_id']:
            key += '#' + o['characteristic_id']
        offer_map[key] = o

    result = []
    for product in catalog['products']:
        pid = product['id']
        if product['characteristics']:
            for ch in product['characteristics']:
                key = pid + '#' + ch['id']
                offer = offer_map.get(key)
                result.append({
                    **product,
                    'characteristic': ch['name'],
                    'prices': offer['prices'] if offer else {},
                    'quantity': offer['quantity'] if offer else 0,
                    'has_offer': offer is not None
                })
        else:
            offer = offer_map.get(pid)
            result.append({
                **product,
                'characteristic': None,
                'prices': offer['prices'] if offer else {},
                'quantity': offer['quantity'] if offer else 0,
                'has_offer': offer is not None
            })
    return result

Маппинг CommerceML → объекты 1С

CommerceML элементОбъект 1СКомментарий
<Группа>Справочник.НоменклатурныеГруппы / Справочник.КатегорииНоменклатурыЗависит от конфигурации
<Товар>Справочник.НоменклатураОсновной справочник товаров
<ХарактеристикаТовара>Справочник.ХарактеристикиНоменклатурыРазмер, цвет, вариант
<Свойство>ПланВидовХарактеристик.ДополнительныеРеквизитыИСведенияПроизвольные свойства
<ТипЦены>Справочник.ВидыЦен (УТ) / Справочник.ТипыЦенНоменклатуры (БП)Розничная, оптовая
<Склад>Справочник.СкладыСкладские остатки
<Контрагент>Справочник.КонтрагентыПокупатели/поставщики
<Документ> (заказ)Документ.ЗаказПокупателя (УТ) / Документ.ЗаказКлиента (ERP)Заказ с сайта
<Цена>РегистрСведений.ЦеныНоменклатурыХранение цен
<Количество> (остаток)РегистрНакопления.ТоварыНаСкладах / СвободныеОстаткиТекущие остатки

Типичные ошибки и проблемы

1. Кодировка windows-1251

Старые выгрузки из 1С могут использовать windows-1251 вместо UTF-8:

# Проверка и перекодировка
with open(filepath, 'rb') as f:
    raw = f.read(100)
if b'encoding="windows-1251"' in raw or b'encoding="Windows-1251"' in raw:
    with open(filepath, 'r', encoding='windows-1251') as f:
        content = f.read()
    # Перезаписать в UTF-8 для стандартного парсинга
    content = content.replace('encoding="windows-1251"', 'encoding="utf-8"')
    content = content.replace('encoding="Windows-1251"', 'encoding="utf-8"')

2. Дубли GUID товаров

Один и тот же <Ид> может встречаться несколько раз — при обновлении выгрузки 1С отправляет полную замену. Всегда используй upsert-логику, а не append.

3. Связь товар#характеристика

В offers.xml идентификатор предложения может быть составным: GUID_товара#GUID_характеристики. Обязательно split('#'):

offer_id = "aaa-bbb-ccc#ddd-eee-fff"
product_id, char_id = offer_id.split('#') if '#' in offer_id else (offer_id, None)

4. Порционная выгрузка

Большие каталоги разбиваются на файлы: import0_1.xml, import0_2.xml... Каждый файл — самостоятельный XML с тем же корневым элементом. Нужно обрабатывать все файлы:

from pathlib import Path

files = sorted(Path(directory).glob('import*.xml'))
all_products = []
for f in files:
    result = parse_catalog(str(f))
    all_products.extend(result['products'])

5. Пустые значения свойств

<ЗначенияСвойства> может содержать пустое <Значение/> — это означает сброс свойства. Не игнорировать:

value = zs.findtext('Значение')  # Может быть None или ''
# None = тег отсутствует, '' = значение сброшено

6. Несовпадение ИД типов цен

GUID типов цен в offers.xml<ТипыЦен> и в самих <Предложение><Цена><ИдТипаЦены> должны совпадать. Если выгрузка неполная, типы цен могут быть не определены.

7. Большие файлы (>100MB)

Для файлов >100MB используй iterparse вместо parse:

import xml.etree.ElementTree as ET

def parse_large_catalog(filepath: str):
    """Потоковый парсинг больших каталогов."""
    for event, elem in ET.iterparse(filepath, events=('end',)):
        if elem.tag == 'Товар':
            yield {
                'id': elem.findtext('Ид'),
                'name': elem.findtext('Наименование'),
                'sku': elem.findtext('Артикул', '')
            }
            elem.clear()  # Освобождаем память

Стандартные имена файлов

ФайлНазначение
import.xmlКаталог товаров (классификатор + товары)
offers.xmlЦены и остатки
orders.xmlЗаказы (выгрузка из сайта в 1С)
import0_1.xmlПорционная выгрузка каталога (часть 1)
offers0_1.xmlПорционная выгрузка предложений (часть 1)

Типичные задачи анализа

ЗадачаЧто делать
Извлечь каталог товаровРазобрать <Классификатор><Группы> + <Каталог><Товары>, построить дерево категорий
Сравнить ценыСопоставить <Предложение> / <Ид> с <Товар> / <Ид>, вывести таблицу товар-цена
Проверить остаткиИзвлечь <Количество> или <Склад КоличествоНаСкладе> из offers.xml
Сверить заказыСопоставить <Документ> / <Товары> с каталогом по GUID, проверить цены и суммы
Найти товары без ценНайти товары из import.xml, отсутствующие в offers.xml по <Ид>
Анализ характеристикИзвлечь <ХарактеристикиТовара>, сгруппировать по товару
Проверить загрузку заказовСверить orders.xml с ответом 1С (статусы, номера по 1С)
Валидация целостностиGUID товаров в offers.xml должны быть в import.xml

Особенности формата

  • GUID идентификаторы: все <Ид> — UUID v4 в формате xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  • Связь товар-предложение: <Предложение>/<Ид> = <Товар>/<Ид> (простой) или <Товар>/<Ид>#<Характеристика>/<Ид> (с характеристиками)
  • Кодировки: обычно UTF-8, реже windows-1251 в старых выгрузках
  • Большие файлы: каталоги с 10 000+ товарами — порционная выгрузка (import0_1.xml, import0_2.xml...)

Anti-patterns

  • Не изменять CommerceML-файлы — только читать и анализировать
  • Не генерировать CommerceML XML — для этого используется 1С:Предприятие
  • Не путать с XML ФНС (2-НДФЛ, 3-НДФЛ) — это другой формат
  • Не парсить регулярками — только XML-парсер (ElementTree, lxml)
  • Не загружать целиком в память файлы >100MB — использовать iterparse
Категория
📊 Документы и расчёты
Платформа
Сам Решу

Попробуйте этот навык

Зарегистрируйтесь и используйте навык «Анализ CommerceML (обмен 1С с сайтом)» бесплатно.