В чем суть проблемы
Нужно начать с того, что между 1С-Битрикс: Управление сайтом и самой 1С-Битрикс: Управление торговлей или 1С-Битрикс: Предприятие есть путаница в терминологии. То, что все люди на сайте называют категориями, в официальной документации 1С-Битрикс: Управление сайтом называется разделами, а в 1С, которая программа учета (неважно, какая редакция), категория, она же раздел, называется "Группа". Но также в 1С есть категории, это некие сущности для удобства использования программой, когда товар из одной группы может относиться к нескольким категориям. Но эти категории из 1С никакого отношения не имеют к категориям (разделам) на сайте.В результате этого возникает путаница, когда разработчики сайта просят 1с-ника выгрузить на сайт категории, а он не понимает, что от него хотят, потому что в его понимании категории нельзя выгрузить на сайт, по крайней мере стандартным модулем обмена, потому что это не группы. А один и тот же товар нельзя поместить в несколько групп, потому что тогда будет неудобно пользоваться 1С-кой, и в таком случае при импорте на сайт создадутся дубли одного и того же товара.
Важно!!! При решении данной проблемы мы не должны костылями переписывать стандартный модуль обмена или как-то дописывать саму 1С.
Задача
Т.е. перед нами стоит задача сделать какую-то надстройку над модулем обмена, которая бы добавляла после импорта товаров из 1С к разделу (который в 1С называется "Группа") дополнительные разделы на сайте (которые в 1С называются "Категории", чтобы товар показывался в нескольких категориях (разделах) – а это уже стандартный функционал битрикса.Как это сделать
1) При обмене 1С с сайтом, создается файл /upload/1c_catalog/import.xml, в котором есть привязка товара к группе и к категориям (смотрим, есть ли там эти привязки).2) Пишем скрипт на php, который должен на сайте в нужном инфоблоке найти товары и сопоставить их с товарами из xml файла по названию. Если название товаров совпали, то скрипт должен товарам на сайте проставить дополнительные разделы. Также нужно учесть, что если такого раздела нет, то нужно его создать, и у товара может быть несколько разделов. Еще учитываем, чтобы при повторном запуске скрипт проверял простановку категорий.
3) Вешаем этот скрипт на крон, чтобы он срабатывал после обмена из 1С и проставлял категории у новых товаров, и отслеживал изменения у текущих.
Скрипт
Ниже код самого скрипта. Базово тут все протестировано и работает. Но важно учитывать, что под каждый сайт его нужно адаптировать: в категориях могут быть разные ненужные символы, возможно какие-то категории нужно исключить из создания и т.д.
Нужна помощь с настройкой?
Если для вас это покажется слишком сложным или нет времени — просто оставьте заявку. Мы подключимся и настроим всё за вас. Пишите на info@prav-site.ru или оставить заявку и мы поможем.
Скопируйте этот код
<?php
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('BX_CRONTAB', true);
@set_time_limit(0);
@ignore_user_abort(true);
@ini_set('memory_limit', '1024M');
$isCli = PHP_SAPI === 'cli';
if (!$isCli) {
@ini_set('output_buffering', 'off');
@ini_set('zlib.output_compression', 0);
while (ob_get_level() > 0) {
@ob_end_flush();
}
header('Content-Type: text/html; charset=UTF-8');
header('X-Accel-Buffering: no');
}
$documentRoot = $_SERVER['DOCUMENT_ROOT'];
if (!$documentRoot && $isCli) {
$documentRoot = realpath(__DIR__ . '/../../');
$_SERVER['DOCUMENT_ROOT'] = $documentRoot;
}
require_once $documentRoot . '/bitrix/modules/main/include/prolog_before.php';
if (function_exists('session_write_close')) {
session_write_close();
}
if (!CModule::IncludeModule('iblock')) {
die('Ошибка: модуль iblock не подключен');
}
$IBLOCK_ID = 123;
$XML_FILE = $documentRoot . '/upload/1c_catalog/import.xml';
$DRY_RUN = true;
if ((string)($_GET['run'] ?? '') === 'Y' || ($isCli && in_array('--run', $argv ?? [], true))) {
$DRY_RUN = false;
}
$MERGE_WITH_EXISTING_SECTIONS = true;
$ONLY_ACTIVE_ELEMENTS = false;
$ROOT_SECTION_ID = 0;
$USE_ONLY_VISIBLE_CATEGORIES = true;
$SKIP_DELETED_XML_PRODUCTS = true;
$UPDATE_EXISTING_SECTION_XML_ID = true;
$VISUAL_UPDATE_EVERY = 25;
$logFile = $documentRoot . '/upload/set_product_sections_from_xml_' . date('Y-m-d_H-i-s') . '.log';
function e($value)
{
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function flushOutput()
{
if (PHP_SAPI !== 'cli') {
echo str_repeat(' ', 4096);
}
@flush();
}
function normalizeName($name)
{
$name = html_entity_decode((string)$name, ENT_QUOTES | ENT_XML1, 'UTF-8');
$name = str_replace(["\xC2\xA0", ' '], ' ', $name);
$name = trim($name);
$name = preg_replace('/\s+/u', ' ', $name);
return mb_strtolower($name, 'UTF-8');
}
function getXmlAttribute(SimpleXMLElement $node, $attrName)
{
foreach ($node->attributes() as $name => $value) {
if ((string)$name === $attrName) {
return trim((string)$value);
}
}
return '';
}
function parseXmlCategory($rawValue)
{
$rawValue = trim((string)$rawValue);
if ($rawValue === '') {
return null;
}
$parts = explode('++++', $rawValue);
$name = trim($parts[0] ?? '');
$xmlId = trim($parts[1] ?? '');
if ($name === '') {
return null;
}
return [
'RAW' => $rawValue,
'NAME' => $name,
'XML_ID' => $xmlId,
];
}
function makeSectionCode($name)
{
$code = CUtil::translit($name, 'ru', [
'max_len' => 120,
'change_case' => 'L',
'replace_space' => '-',
'replace_other' => '-',
'delete_repeat_replace' => true,
'safe_chars' => '',
]);
$code = trim($code, '-');
return $code ?: 'section';
}
function sectionCodeExists($iblockId, $code)
{
$res = CIBlockSection::GetList(
[],
[
'IBLOCK_ID' => $iblockId,
'=CODE' => $code,
'CHECK_PERMISSIONS' => 'N',
],
false,
['ID']
);
return (bool)$res->Fetch();
}
function makeUniqueSectionCode($iblockId, $name)
{
$baseCode = makeSectionCode($name);
$code = $baseCode;
$counter = 2;
while (sectionCodeExists($iblockId, $code)) {
$code = $baseCode . '-' . $counter;
$counter++;
}
return $code;
}
function renderVisualStart($iblockId, $xmlFile, $dryRun, $logFile)
{
if (PHP_SAPI === 'cli') {
return;
}
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Привязка товаров к разделам из XML</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 32px;
font-family: Arial, sans-serif;
background: #f4f7fb;
color: #1f2937;
}
.ps-import-page {
max-width: 1180px;
margin: 0 auto;
}
.ps-import-header {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: flex-start;
margin-bottom: 24px;
}
.ps-import-title {
margin: 0 0 8px;
font-size: 28px;
line-height: 1.25;
color: #111827;
}
.ps-import-subtitle {
margin: 0;
color: #6b7280;
font-size: 15px;
line-height: 1.5;
}
.ps-import-mode {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 999px;
font-size: 14px;
font-weight: 700;
}
.ps-import-mode--test {
background: #fff7ed;
color: #c2410c;
border: 1px solid #fed7aa;
}
.ps-import-mode--run {
background: #ecfdf5;
color: #047857;
border: 1px solid #a7f3d0;
}
.ps-import-panel {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 18px;
box-shadow: 0 12px 34px rgba(15, 23, 42, 0.08);
padding: 24px;
margin-bottom: 20px;
}
.ps-import-progress-top {
display: flex;
justify-content: space-between;
gap: 20px;
margin-bottom: 12px;
color: #374151;
font-size: 15px;
}
.ps-import-progress-percent {
font-weight: 800;
color: #111827;
}
.ps-import-progress {
width: 100%;
height: 18px;
background: #eef2f7;
border-radius: 999px;
overflow: hidden;
position: relative;
}
.ps-import-progress__bar {
width: 0%;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #2563eb, #22c55e);
transition: width 0.25s ease;
}
.ps-import-current {
margin-top: 14px;
padding: 14px 16px;
background: #f9fafb;
border: 1px solid #edf0f3;
border-radius: 14px;
color: #4b5563;
font-size: 14px;
line-height: 1.5;
}
.ps-import-current strong {
display: block;
margin-bottom: 4px;
color: #111827;
}
.ps-import-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-bottom: 20px;
}
.ps-import-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 18px;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05);
}
.ps-import-card__label {
color: #6b7280;
font-size: 13px;
line-height: 1.35;
margin-bottom: 8px;
}
.ps-import-card__value {
color: #111827;
font-size: 26px;
line-height: 1;
font-weight: 800;
}
.ps-import-info {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-bottom: 20px;
}
.ps-import-info__item {
padding: 14px 16px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 14px;
font-size: 14px;
line-height: 1.45;
color: #4b5563;
word-break: break-word;
}
.ps-import-info__item strong {
display: block;
color: #111827;
margin-bottom: 4px;
}
.ps-import-log {
height: 360px;
overflow: auto;
background: #0f172a;
color: #d1d5db;
border-radius: 16px;
padding: 16px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
}
.ps-import-log__item {
padding: 4px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.ps-import-log__item--error {
color: #fca5a5;
}
.ps-import-log__item--success {
color: #86efac;
}
.ps-import-log__item--warning {
color: #fde68a;
}
.ps-import-finish {
display: none;
margin-top: 20px;
padding: 18px;
border-radius: 16px;
background: #ecfdf5;
border: 1px solid #a7f3d0;
color: #065f46;
font-size: 15px;
line-height: 1.5;
}
.ps-import-finish.is-active {
display: block;
}
@media (max-width: 900px) {
body {
padding: 18px;
}
.ps-import-header {
display: block;
}
.ps-import-mode {
margin-top: 14px;
}
.ps-import-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ps-import-info {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.ps-import-grid {
grid-template-columns: 1fr;
}
.ps-import-panel {
padding: 18px;
}
.ps-import-title {
font-size: 23px;
}
}
</style>
</head>
<body>
<div class="ps-import-page">
<div class="ps-import-header">
<div>
<h1 class="ps-import-title">Привязка товаров к разделам из XML</h1>
<p class="ps-import-subtitle">
Скрипт ищет товары по названию, проверяет разделы, создает недостающие и добавляет товару только отсутствующие привязки.
</p>
</div>
<div class="ps-import-mode <?= $dryRun ? 'ps-import-mode--test' : 'ps-import-mode--run'; ?>">
<?= $dryRun ? 'Тестовый режим' : 'Боевой режим'; ?>
</div>
</div>
<div class="ps-import-info">
<div class="ps-import-info__item">
<strong>Инфоблок</strong>
ID <?= (int)$iblockId; ?>
</div>
<div class="ps-import-info__item">
<strong>XML-файл</strong>
<?= e($xmlFile); ?>
</div>
<div class="ps-import-info__item">
<strong>Файл лога</strong>
<?= e($logFile); ?>
</div>
<div class="ps-import-info__item">
<strong>Запуск изменений</strong>
<?= $dryRun ? 'Для боевого запуска добавьте ?run=Y' : 'Изменения вносятся на сайт'; ?>
</div>
</div>
<div class="ps-import-grid">
<div class="ps-import-card">
<div class="ps-import-card__label">Всего товаров XML</div>
<div class="ps-import-card__value" id="psTotal">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Обработано</div>
<div class="ps-import-card__value" id="psProcessed">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Совпало</div>
<div class="ps-import-card__value" id="psMatched">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Обновлено</div>
<div class="ps-import-card__value" id="psUpdated">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Уже привязаны</div>
<div class="ps-import-card__value" id="psAlreadyAssigned">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Без категорий XML</div>
<div class="ps-import-card__value" id="psWithoutXmlCategories">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Не найдено</div>
<div class="ps-import-card__value" id="psNotFound">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Создано разделов</div>
<div class="ps-import-card__value" id="psCreatedSections">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Дубли на сайте</div>
<div class="ps-import-card__value" id="psDuplicates">0</div>
</div>
<div class="ps-import-card">
<div class="ps-import-card__label">Ошибки</div>
<div class="ps-import-card__value" id="psErrors">0</div>
</div>
</div>
<div class="ps-import-panel">
<div class="ps-import-progress-top">
<div>Ход обработки</div>
<div class="ps-import-progress-percent" id="psPercent">0%</div>
</div>
<div class="ps-import-progress">
<div class="ps-import-progress__bar" id="psProgressBar"></div>
</div>
<div class="ps-import-current">
<strong>Текущий товар</strong>
<span id="psCurrentProduct">Подготовка...</span>
</div>
</div>
<div class="ps-import-panel">
<div class="ps-import-log" id="psLog"></div>
<div class="ps-import-finish" id="psFinish"></div>
</div>
</div>
<script>
function psSetText(id, value) {
var el = document.getElementById(id);
if (el) {
el.textContent = value;
}
}
function psUpdateProgress(data) {
var percent = data.total > 0 ? Math.round((data.processed / data.total) * 100) : 0;
if (percent > 100) {
percent = 100;
}
psSetText('psTotal', data.total);
psSetText('psProcessed', data.processed);
psSetText('psMatched', data.matched);
psSetText('psUpdated', data.updated);
psSetText('psAlreadyAssigned', data.alreadyAssigned);
psSetText('psWithoutXmlCategories', data.withoutXmlCategories);
psSetText('psNotFound', data.notFound);
psSetText('psCreatedSections', data.createdSections);
psSetText('psDuplicates', data.duplicates);
psSetText('psErrors', data.errors);
psSetText('psPercent', percent + '%');
psSetText('psCurrentProduct', data.currentProduct || '');
var bar = document.getElementById('psProgressBar');
if (bar) {
bar.style.width = percent + '%';
}
}
function psAppendLog(message, type) {
var log = document.getElementById('psLog');
if (!log) {
return;
}
var item = document.createElement('div');
item.className = 'ps-import-log__item';
if (type) {
item.className += ' ps-import-log__item--' + type;
}
item.textContent = message;
log.appendChild(item);
while (log.children.length > 250) {
log.removeChild(log.children[0]);
}
log.scrollTop = log.scrollHeight;
}
function psFinish(message) {
var finish = document.getElementById('psFinish');
if (finish) {
finish.textContent = message;
finish.classList.add('is-active');
}
}
</script>
<?php
flushOutput();
}
function visualProgress($processed, $total, $currentProduct, array $stats)
{
if (PHP_SAPI === 'cli') {
return;
}
$data = [
'processed' => (int)$processed,
'total' => (int)$total,
'currentProduct' => (string)$currentProduct,
'matched' => (int)($stats['matched_products'] ?? 0),
'updated' => (int)($stats['updated_elements'] ?? 0),
'alreadyAssigned' => (int)($stats['already_assigned_products'] ?? 0),
'withoutXmlCategories' => (int)($stats['products_without_xml_categories'] ?? 0),
'notFound' => (int)($stats['not_found_products'] ?? 0),
'createdSections' => (int)($stats['created_sections'] ?? 0),
'duplicates' => (int)($stats['duplicate_site_products'] ?? 0),
'errors' => (int)($stats['errors'] ?? 0),
];
echo '<script>psUpdateProgress(' . json_encode($data, JSON_UNESCAPED_UNICODE) . ');</script>' . PHP_EOL;
flushOutput();
}
function visualLog($message, $type = '')
{
if (PHP_SAPI === 'cli') {
return;
}
echo '<script>psAppendLog(' .
json_encode((string)$message, JSON_UNESCAPED_UNICODE) .
', ' .
json_encode((string)$type, JSON_UNESCAPED_UNICODE) .
');</script>' . PHP_EOL;
flushOutput();
}
function visualFinish($message)
{
if (PHP_SAPI === 'cli') {
return;
}
echo '<script>psFinish(' . json_encode((string)$message, JSON_UNESCAPED_UNICODE) . ');</script>' . PHP_EOL;
echo '</body></html>';
flushOutput();
}
function logMessage($message, $type = '', $showInUi = true)
{
global $logFile;
$line = '[' . date('Y-m-d H:i:s') . '] ' . $message;
if (PHP_SAPI === 'cli') {
echo $line . PHP_EOL;
}
file_put_contents($logFile, $line . PHP_EOL, FILE_APPEND);
if ($showInUi) {
visualLog($line, $type);
}
}
function loadSiteProducts($iblockId, $onlyActive = false)
{
$products = [];
$duplicates = [];
$filter = [
'IBLOCK_ID' => $iblockId,
'CHECK_PERMISSIONS' => 'N',
];
if ($onlyActive) {
$filter['ACTIVE'] = 'Y';
}
$res = CIBlockElement::GetList(
['ID' => 'ASC'],
$filter,
false,
false,
['ID', 'IBLOCK_ID', 'NAME']
);
while ($item = $res->Fetch()) {
$key = normalizeName($item['NAME']);
if ($key === '') {
continue;
}
$products[$key][] = [
'ID' => (int)$item['ID'],
'NAME' => $item['NAME'],
];
}
foreach ($products as $key => $items) {
if (count($items) > 1) {
$duplicates[$key] = $items;
}
}
return [$products, $duplicates];
}
function loadSiteSections($iblockId)
{
$sectionsByXmlId = [];
$sectionsByName = [];
$sectionsDataById = [];
$res = CIBlockSection::GetList(
['LEFT_MARGIN' => 'ASC'],
[
'IBLOCK_ID' => $iblockId,
'CHECK_PERMISSIONS' => 'N',
],
false,
['ID', 'IBLOCK_ID', 'NAME', 'XML_ID', 'CODE', 'IBLOCK_SECTION_ID']
);
while ($section = $res->Fetch()) {
$id = (int)$section['ID'];
$xmlId = trim((string)$section['XML_ID']);
$nameKey = normalizeName($section['NAME']);
$sectionsDataById[$id] = [
'ID' => $id,
'NAME' => $section['NAME'],
'XML_ID' => $xmlId,
'CODE' => $section['CODE'],
'IBLOCK_SECTION_ID' => (int)$section['IBLOCK_SECTION_ID'],
];
if ($xmlId !== '') {
$sectionsByXmlId[$xmlId] = $id;
}
if ($nameKey !== '' && !isset($sectionsByName[$nameKey])) {
$sectionsByName[$nameKey] = $id;
}
}
return [$sectionsByXmlId, $sectionsByName, $sectionsDataById];
}
function updateSectionXmlIdIfNeeded($sectionId, $xmlId, &$sectionsByXmlId, &$sectionsDataById)
{
global $UPDATE_EXISTING_SECTION_XML_ID;
$xmlId = trim((string)$xmlId);
if (!$UPDATE_EXISTING_SECTION_XML_ID || $sectionId <= 0 || $xmlId === '') {
return;
}
$currentXmlId = trim((string)($sectionsDataById[$sectionId]['XML_ID'] ?? ''));
if ($currentXmlId !== '') {
return;
}
$section = new CIBlockSection();
$updated = $section->Update($sectionId, [
'XML_ID' => $xmlId,
]);
if ($updated) {
$sectionsByXmlId[$xmlId] = $sectionId;
$sectionsDataById[$sectionId]['XML_ID'] = $xmlId;
logMessage('Разделу ID ' . $sectionId . ' добавлен XML_ID: "' . $xmlId . '"', 'success', false);
}
}
function getOrCreateSectionId(
$iblockId,
array $category,
&$sectionsByXmlId,
&$sectionsByName,
&$sectionsDataById,
$parentSectionId,
$dryRun,
&$stats
) {
$sectionName = trim($category['NAME']);
$sectionXmlId = trim($category['XML_ID']);
if ($sectionXmlId !== '' && isset($sectionsByXmlId[$sectionXmlId])) {
return (int)$sectionsByXmlId[$sectionXmlId];
}
$nameKey = normalizeName($sectionName);
if ($nameKey !== '' && isset($sectionsByName[$nameKey])) {
$sectionId = (int)$sectionsByName[$nameKey];
if (!$dryRun) {
updateSectionXmlIdIfNeeded($sectionId, $sectionXmlId, $sectionsByXmlId, $sectionsDataById);
}
return $sectionId;
}
if ($dryRun) {
logMessage('TEST: будет создан раздел: "' . $sectionName . '" XML_ID: "' . $sectionXmlId . '"', 'warning');
return 0;
}
$section = new CIBlockSection();
$fields = [
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
'NAME' => $sectionName,
'XML_ID' => $sectionXmlId,
'CODE' => makeUniqueSectionCode($iblockId, $sectionName),
'SORT' => 500,
];
if ((int)$parentSectionId > 0) {
$fields['IBLOCK_SECTION_ID'] = (int)$parentSectionId;
}
$sectionId = (int)$section->Add($fields);
if ($sectionId <= 0) {
$stats['errors']++;
logMessage('Ошибка создания раздела "' . $sectionName . '": ' . $section->LAST_ERROR, 'error');
return 0;
}
if ($sectionXmlId !== '') {
$sectionsByXmlId[$sectionXmlId] = $sectionId;
}
if ($nameKey !== '') {
$sectionsByName[$nameKey] = $sectionId;
}
$sectionsDataById[$sectionId] = [
'ID' => $sectionId,
'NAME' => $sectionName,
'XML_ID' => $sectionXmlId,
'CODE' => $fields['CODE'],
'IBLOCK_SECTION_ID' => (int)$parentSectionId,
];
$stats['created_sections']++;
logMessage('Создан раздел: ID ' . $sectionId . ', "' . $sectionName . '", XML_ID: "' . $sectionXmlId . '"', 'success');
return $sectionId;
}
function getElementSectionIds($elementId)
{
$sectionIds = [];
$res = CIBlockElement::GetElementGroups($elementId, true, ['ID']);
while ($section = $res->Fetch()) {
$sectionIds[] = (int)$section['ID'];
}
return array_values(array_unique(array_filter($sectionIds)));
}
function readXmlProducts($xmlFile)
{
global $USE_ONLY_VISIBLE_CATEGORIES;
global $SKIP_DELETED_XML_PRODUCTS;
$products = [];
$sections = [];
$stats = [
'xml_products_total' => 0,
'xml_products_with_categories' => 0,
'xml_products_skipped_deleted' => 0,
'xml_products_without_name' => 0,
'xml_products_without_categories' => 0,
];
if (!file_exists($xmlFile)) {
throw new RuntimeException('XML-файл не найден: ' . $xmlFile);
}
libxml_use_internal_errors(true);
$reader = new XMLReader();
if (!$reader->open($xmlFile)) {
throw new RuntimeException('Не удалось открыть XML-файл: ' . $xmlFile);
}
while ($reader->read()) {
if ($reader->nodeType !== XMLReader::ELEMENT || $reader->localName !== 'Товар') {
continue;
}
$nodeXml = $reader->readOuterXML();
if (!$nodeXml) {
continue;
}
$productXml = simplexml_load_string($nodeXml, 'SimpleXMLElement', LIBXML_NOCDATA);
if (!$productXml) {
continue;
}
$stats['xml_products_total']++;
$name = trim((string)$productXml->{'Наименование'});
$status = trim((string)$productXml->{'Статус'});
if ($name === '') {
$stats['xml_products_without_name']++;
continue;
}
if ($SKIP_DELETED_XML_PRODUCTS && normalizeName($status) === 'удален') {
$stats['xml_products_skipped_deleted']++;
continue;
}
$productKey = normalizeName($name);
if (!isset($products[$productKey])) {
$products[$productKey] = [
'XML_ID' => trim((string)$productXml->{'Ид'}),
'NAME' => $name,
'STATUS' => $status,
'CATEGORIES' => [],
];
}
if (!isset($productXml->{'Категории'}->{'Категория'})) {
$stats['xml_products_without_categories']++;
continue;
}
$hasCategories = false;
foreach ($productXml->{'Категории'}->{'Категория'} as $categoryNode) {
$showAttr = getXmlAttribute($categoryNode, 'Отображать');
if ($USE_ONLY_VISIBLE_CATEGORIES && $showAttr !== '' && mb_strtolower($showAttr, 'UTF-8') !== 'true') {
continue;
}
$category = parseXmlCategory((string)$categoryNode);
if (!$category) {
continue;
}
$categoryKey = $category['XML_ID'] !== ''
? $category['XML_ID']
: normalizeName($category['NAME']);
$products[$productKey]['CATEGORIES'][$categoryKey] = $category;
$sections[$categoryKey] = $category;
$hasCategories = true;
}
if ($hasCategories) {
$stats['xml_products_with_categories']++;
} else {
$stats['xml_products_without_categories']++;
}
}
$reader->close();
return [$products, $sections, $stats];
}
renderVisualStart($IBLOCK_ID, $XML_FILE, $DRY_RUN, $logFile);
logMessage('Старт скрипта');
logMessage('Инфоблок: ' . $IBLOCK_ID);
logMessage('XML-файл: ' . $XML_FILE);
logMessage('Режим: ' . ($DRY_RUN ? 'ТЕСТОВЫЙ, изменения не вносятся' : 'БОЕВОЙ, изменения будут внесены'));
try {
[$xmlProducts, $xmlSections, $xmlStats] = readXmlProducts($XML_FILE);
} catch (Throwable $e) {
logMessage('Критическая ошибка: ' . $e->getMessage(), 'error');
visualFinish('Скрипт остановлен из-за ошибки: ' . $e->getMessage());
die();
}
logMessage('Товаров в XML всего: ' . $xmlStats['xml_products_total']);
logMessage('Товаров XML с категориями: ' . $xmlStats['xml_products_with_categories']);
logMessage('Пропущено удаленных товаров XML: ' . $xmlStats['xml_products_skipped_deleted']);
logMessage('Товаров XML без названия: ' . $xmlStats['xml_products_without_name']);
logMessage('Товаров XML без категорий: ' . $xmlStats['xml_products_without_categories']);
logMessage('Уникальных товаров XML по названию: ' . count($xmlProducts));
logMessage('Уникальных категорий XML: ' . count($xmlSections));
[$siteProducts, $siteProductDuplicates] = loadSiteProducts($IBLOCK_ID, $ONLY_ACTIVE_ELEMENTS);
[$sectionsByXmlId, $sectionsByName, $sectionsDataById] = loadSiteSections($IBLOCK_ID);
logMessage('Товаров на сайте загружено в память: ' . count($siteProducts));
logMessage('Дублей товаров на сайте по названию: ' . count($siteProductDuplicates));
logMessage('Разделов на сайте загружено в память: ' . count($sectionsDataById));
$stats = [
'matched_products' => 0,
'not_found_products' => 0,
'duplicate_site_products' => 0,
'products_without_xml_categories' => 0,
'products_without_resolved_sections' => 0,
'already_assigned_products' => 0,
'updated_elements' => 0,
'created_sections' => 0,
'errors' => 0,
];
$totalForProcessing = count($xmlProducts);
$processed = 0;
visualProgress($processed, $totalForProcessing, 'Начинаем обработку товаров...', $stats);
foreach ($xmlProducts as $productKey => $xmlProduct) {
$processed++;
$currentProductName = $xmlProduct['NAME'];
if (empty($xmlProduct['CATEGORIES'])) {
$stats['products_without_xml_categories']++;
logMessage(
'Пропуск: у товара в XML нет категорий: "' . $xmlProduct['NAME'] . '"',
'warning',
false
);
if ($processed % $VISUAL_UPDATE_EVERY === 0 || $processed === $totalForProcessing) {
visualProgress($processed, $totalForProcessing, $currentProductName, $stats);
}
continue;
}
if (!isset($siteProducts[$productKey])) {
$stats['not_found_products']++;
logMessage(
'Не найден товар на сайте: "' . $xmlProduct['NAME'] . '"',
'warning',
false
);
if ($processed % $VISUAL_UPDATE_EVERY === 0 || $processed === $totalForProcessing) {
visualProgress($processed, $totalForProcessing, $currentProductName, $stats);
}
continue;
}
if (count($siteProducts[$productKey]) > 1) {
$stats['duplicate_site_products']++;
logMessage(
'Внимание: на сайте найдено несколько товаров с названием "' . $xmlProduct['NAME'] . '". Будут обработаны все найденные товары.',
'warning'
);
}
$xmlSectionIds = [];
foreach ($xmlProduct['CATEGORIES'] as $category) {
$sectionId = getOrCreateSectionId(
$IBLOCK_ID,
$category,
$sectionsByXmlId,
$sectionsByName,
$sectionsDataById,
$ROOT_SECTION_ID,
$DRY_RUN,
$stats
);
if ($sectionId > 0) {
$xmlSectionIds[] = $sectionId;
}
}
$xmlSectionIds = array_values(array_unique(array_filter($xmlSectionIds)));
if (!$DRY_RUN && empty($xmlSectionIds)) {
$stats['products_without_resolved_sections']++;
logMessage(
'Пропуск: не удалось определить или создать разделы для товара: "' . $xmlProduct['NAME'] . '"',
'error'
);
if ($processed % $VISUAL_UPDATE_EVERY === 0 || $processed === $totalForProcessing) {
visualProgress($processed, $totalForProcessing, $currentProductName, $stats);
}
continue;
}
foreach ($siteProducts[$productKey] as $siteProduct) {
$elementId = (int)$siteProduct['ID'];
if ($DRY_RUN) {
$categoryNames = [];
foreach ($xmlProduct['CATEGORIES'] as $category) {
$categoryNames[] = $category['NAME'];
}
logMessage(
'TEST: товар ID ' . $elementId .
' "' . $siteProduct['NAME'] . '" будет проверен на привязку к разделам: ' .
implode(', ', $categoryNames),
'success',
false
);
$stats['matched_products']++;
continue;
}
$currentSectionIds = getElementSectionIds($elementId);
$missingSectionIds = array_values(array_diff($xmlSectionIds, $currentSectionIds));
if (empty($missingSectionIds)) {
$stats['already_assigned_products']++;
$stats['matched_products']++;
logMessage(
'Пропуск: товар ID ' . $elementId .
' "' . $siteProduct['NAME'] . '" уже привязан ко всем нужным разделам',
'success',
false
);
continue;
}
if ($MERGE_WITH_EXISTING_SECTIONS) {
$finalSectionIds = array_values(array_unique(array_merge($currentSectionIds, $missingSectionIds)));
} else {
$finalSectionIds = $xmlSectionIds;
}
$result = CIBlockElement::SetElementSection($elementId, $finalSectionIds);
if ($result === false) {
$stats['errors']++;
logMessage(
'Ошибка привязки разделов к товару ID ' . $elementId . ': "' . $siteProduct['NAME'] . '"',
'error'
);
continue;
}
$stats['updated_elements']++;
$stats['matched_products']++;
logMessage(
'Обновлен товар ID ' . $elementId .
' "' . $siteProduct['NAME'] . '". Добавлены разделы ID: ' .
implode(', ', $missingSectionIds) .
'. Итоговые разделы ID: ' .
implode(', ', $finalSectionIds),
'success',
false
);
}
if ($processed % $VISUAL_UPDATE_EVERY === 0 || $processed === $totalForProcessing) {
visualProgress($processed, $totalForProcessing, $currentProductName, $stats);
}
}
visualProgress($processed, $totalForProcessing, 'Обработка завершена', $stats);
logMessage('Готово', 'success');
logMessage('Совпавших товаров обработано: ' . $stats['matched_products']);
logMessage('Товаров из XML не найдено на сайте: ' . $stats['not_found_products']);
logMessage('Товаров без категорий в XML: ' . $stats['products_without_xml_categories']);
logMessage('Дублей товаров на сайте по названию: ' . $stats['duplicate_site_products']);
logMessage('Товаров без определенных разделов: ' . $stats['products_without_resolved_sections']);
logMessage('Товаров, которые уже были привязаны к нужным разделам: ' . $stats['already_assigned_products']);
logMessage('Создано разделов: ' . $stats['created_sections']);
logMessage('Обновлено элементов: ' . $stats['updated_elements']);
logMessage('Ошибок: ' . $stats['errors']);
logMessage('Лог: ' . $logFile);
visualFinish('Обработка завершена. Подробный лог записан в файл: ' . $logFile);
require_once $documentRoot . '/bitrix/modules/main/include/epilog_after.php';
Нужна помощь с настройкой?
Если для вас это покажется слишком сложным или нет времени — просто оставьте заявку. Мы подключимся и настроим всё за вас. Пишите на info@prav-site.ru или оставить заявку и мы поможем.
Поделитесь с друзьями

