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