
Привет, Хабр! Меня зовут Алексей Яриков, я ведущий разработчик в команде внешних сайтов НЛМК. Мы занимаемся разработкой и поддержкой веб-платформ компании на Bitrix, обеспечивая их стабильность, производительность и удобство для пользователей.
Актуальность данных является ключевым фактором для успешного функционирования различных аспектов бизнеса — от интернет-магазинов до сложных корпоративных систем. Информация может изменяться по ряду причин, таких как обновление данных поставщиками, изменения цен, актуализация описаний товаров или услуг и многое другое. Несвоевременное обновление данных может привести к ошибкам в обработке заказов, недостоверной информации для пользователей и возможным финансовым потерям.
Во многих наших проектах мы используем CMS Bitrix. Довольно часто требуются небольшие интеграции, позволяющие точно и оперативно предоставлять критически важную информацию. Для эффективного управления данными и синхронизации с внешними источниками важно настроить автоматизированный процесс, который будет регулярно обновлять информацию из внешних источников. Один из эффективных способов реализации такого решения — использование агентов Bitrix.
Обычно для автоматизации периодических задач на веб-сайтах используют Cron-задачи. Однако настройка и поддержка крон-задач могут требовать дополнительных усилий, особенно в условиях ограниченного доступа к серверной части хостинга. У Bitrix есть встроенный функционал агентов, который отлично подходит для мелких и простых интеграций, таких как обновление данных. Агенты Bitrix предоставляют готовый механизм автоматического выполнения задач, который легко настроить и поддерживать в пределах самой CMS без дополнительных внешних инструментов.
Проблема при интеграции XML-файлов с FTP
Сами по себе XML-файлы, содержащие актуальные данные, могут иметь значительный объем, включая информацию о тысячах товаров или других объектах. При подходе к задаче мы не знали ни размеров файлов, ни их количества. XML-файлы периодически выгружались в определенное место на FTP-сервер, могли быть изменены, использовались не только нашей интеграцией, и поэтому не удалялись. Далее выяснилось, что могли быть выгружены файлы с датой актуальности на несколько дней вперёд, которые загружать не нужно.
Настройка агентов Bitrix для интеграции XML
Для эффективного решения данной задачи используются два агента Bitrix, каждый из которых отвечает за свою часть процесса:
Периодический агент для регулярного опроса FTP-сервера и скачивания XML-файлов. Этот агент автоматически запускается через заданные интервалы времени и выполняет последовательность действий по подключению к FTP-серверу, проверке наличия новых файлов и их скачиванию.
Разовый агент для обработки скачанных XML-файлов и обновления данных в базе данных Bitrix. Этот агент ставится в очередь после успешного скачивания файла, запускается один раз и отвечает за процесс парсинга XML, обновления данных в системе, удаление скачанных обработанных файлов. Во время парсинга агент проверяет даты прайс-листов в файле и пропускает его, если дата ещё не наступила. Важно, чтобы агент не ставился в очередь повторно, если он уже там.
Для решения проблемы с настройками интеграции, управления и мониторинга ею было решено обернуть агенты в модуль. Вынесение параметров интеграции в настройки модуля позволило указать данные для подключения к FTP-серверу, вывести краткую информацию по последней запущенной интеграции и разместить дополнительные настройки для типов цен и прочее. Также настройки в таком формате позволят вносить коррективы в процесс импорта без изменения кода.
Осталось решить последнюю проблему — определить, какие файлы уже загружены, а какие ещё нет, поскольку список XML-файлов на FTP-сервере очищается редко. Было решено создать highload-блок, в котором хранится информация об импортируемых файлах. В частности, название файла, дату и время его модификации, дату импорта. Название файла и его дата модификации являются идентификатором файла. Хранение имени файла и даты его модификации в HL-блоке позволяет точно определить, были ли данные обновлены на FTP. Если файл с таким же именем, но с более поздней датой модификации появляется на сервере, он снова загружается и импортируется.
С учетом выше сказанного процесс импорта выглядит следующим образом:

Для мониторинга процесса импорта и отладки все этапы сопровождаются логированием с разными уровнями детализации. Уровень логирования указан в настройках модуля. Переключение уровня логирования позволяет в любой момент увидеть, что произошло с процессом, если что-то пошло не так.
Пример кода периодического агента
class PriceImport
{
public static function importFiles(): string
{
$hasNewPrices = false;
$origin = new DateTime();
try {
$hasNewPrices = (new PriceImporter())->execute();
} catch (Throwable $e) {
Logger::getInstance()->error(
Loc::getMessage('PRICE_IMPORT_ERROR'),
[
'MODULE_ID' => Options::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_PROCESS_ERROR->name,
'ITEM_ID' => __METHOD__,
'ERROR' => $e->getMessage(),
'TRACE' => current($e->getTrace())
]
);
} finally {
Logger::getInstance()->notice(
Loc::getMessage('PRICE_IMPORT_COMPLETED'),
[
'MODULE_ID' => Options::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_PROCESS->name,
'ITEM_ID' => __METHOD__,
'PROCESS_TIME' => $origin->diff(new DateTime())->format(Loc::getMessage('PRICE_IMPORT_PROCESS_TIME_FORMAT')),
]
);
}
$updateAgent = sprintf('\\%s::updateElements();', PriceUpdate::class);
if ($hasNewPrices && !Agent::exist($updateAgent)) {
CAgent::AddAgent($updateAgent, Options::getModuleId(), 'Y', 3600);
}
return '\\' . __METHOD__ . '();';
}
}
Пример кода одноразового агента
class PriceUpdate
{
public static function updateElements(): void
{
$origin = new DateTime();
try {
(new PriceUpdater())->execute();
} catch (Throwable $e) {
Logger::getInstance()->error(
Loc::getMessage('PRICE_UPDATE_ERROR'),
[
'MODULE_ID' => Options::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_PROCESS_ERROR->name,
'ITEM_ID' => __METHOD__,
'ERROR' => $e->getMessage(),
'TRACE' => current($e->getTrace())
]
);
} finally {
Logger::getInstance()->notice(
Loc::getMessage('PRICE_UPDATE_COMPLETED'),
[
'MODULE_ID' => Options::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_PROCESS->name,
'ITEM_ID' => __METHOD__,
'PROCESS_TIME' => $origin->diff(new DateTime())->format(Loc::getMessage('PRICE_UPDATE_PROCESS_TIME_FORMAT')),
]
);
}
}
}
Пример кода чтения файлов на FTP-сервере
protected function readExternalDir(): static
{
try {
$this->externalDirList = $this->ftp->dirList($this->externalPath);
} catch (ArgumentNullException|SystemException $e) {
$this->ftp->close();
Logger::getInstance()->error(
Loc::getMessage('PRICE_IMPORTER_CONNECTION_ERROR'),
[
'MODULE_ID' => ModuleOptions::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_FTP_ERROR->name,
'ITEM_ID' => __METHOD__,
'ERROR' => $e->getMessage(),
'TRACE' => current($e->getTrace())
]
);
}
if (empty($this->externalDirList)) {
$this->externalDirList = [];
$this->ftp->close();
Logger::getInstance()->error(
Loc::getMessage('PRICE_IMPORTER_EXTERNAL_DIR_ERROR', ['#DIR#' => $this->externalPath]),
[
'MODULE_ID' => ModuleOptions::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_IMPORT_ERROR->name,
'ITEM_ID' => __METHOD__,
]
);
}
return $this;
}
Пример кода получения файлов
protected function receiveFiles(): void
{
foreach ($this->externalFilesInfo as $arInfo) {
try {
$success = $this->ftp->receive(
$this->localPath . $arInfo['name'],
$this->externalPath . $arInfo['name'],
);
if ($success) {
$this->receivedFiles[] = $arInfo['name'];
} else {
Logger::getInstance()->error(
Loc::getMessage('PRICE_IMPORTER_SAVE_FILE_ERROR', ['#FILE#' => $arInfo['name']]),
[
'MODULE_ID' => ModuleOptions::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_IMPORT_ERROR->name,
'ITEM_ID' => __METHOD__,
'EXTERNAL_FILE' => $this->externalPath . $arInfo['name'],
]
);
}
} catch (ArgumentNullException|SystemException $e) {
Logger::getInstance()->error(
Loc::getMessage('PRICE_IMPORTER_RECEIVE_FILE_ERROR'),
[
'MODULE_ID' => ModuleOptions::getModuleId(),
'CODE' => LoggerCodes::INTEGRATION_FTP_ERROR->name,
'ITEM_ID' => __METHOD__,
'EXTERNAL_FILE' => $this->externalPath . $arInfo['name'],
'ERROR' => $e->getMessage(),
'TRACE' => current($e->getTrace())
]
);
}
}
$this->ftp->close();
Option::set(
ModuleOptions::getModuleId(),
'last_import_date',
date(ModuleOptions::OPTIONS_DATETIME_FORMAT)
);
Logger::getInstance()->notice(
Loc::getMessage('PRICE_IMPORTER_RECEIVE_SUCCESS'),
[
'MODULE_ID' => ModuleOptions::getModuleId(),
'CODE' => LoggerCodes::ECOTECH_INTEGRATION_PROCESS->name,
'ITEM_ID' => __METHOD__,
'RECEIVED_FILES' => $this->receivedFiles,
]
);
}
Заключение
Таким образом, мы разработали простое в настройке и удобное в использовании решение для интеграции данных, основанное на штатных возможностях CMS Bitrix. Оно позволяет поддерживать актуальность цен на сайте, минимизируя риски ошибок. Разделение процесса на два агента — периодического для получения данных и разового для их обработки — повысило стабильность работы и упростило масштабирование системы. В результате компания получает надёжный и гибкий механизм обновления данных, который снижает вероятность потерь и повышает уровень удовлетворённости клиентов.
Комментарии (5)
3efir4ik
03.06.2025 12:09А как вы парсили большую xmlну ? Не боитесь уйти в out of memory?
Alexey_Yarikov Автор
03.06.2025 12:09У нас на проекте, к счастью, нет необходимости загрузки крупных XML-файлов, но в моей практике приходилось работать с XML размером порядка 18 ГБ. Тогда мы реализовывали обработку, читая файл по частям — по несколько килобайт — и передавали эти участки событийно-ориентированному парсеру (SAX), что позволяло контролировать потребление памяти и избегать её переполнения.
Сейчас для таких задач, как правило, используют XMLReader — это встроенный в PHP потоковый парсер, который позволяет последовательно проходить по структуре XML-документа, извлекая нужные узлы без загрузки всего файла или даже всего дерева в память. Это эффективное и полностью безопасное с точки зрения ресурсов решение.
Pavel-Lukyanov
03.06.2025 12:09Ну будет ошибка в логах, в следующий раз просто добавят set_time_limit(99999); и ini_set('memory_limit', '9999999M');. Зачем очереди периозбретать и следить за потреблением ресурсов.
Подумаешь консистентность данных потеряется при падении скрипта из-за отсутствия транзакций.
P.S. FTP сервер - это сервер на котором включен протокол передачи данных FTP?Alexey_Yarikov Автор
03.06.2025 12:09Нам пока не приходилось грузить в память многогигабайтные XML-файлы. Но даже если такая задача возникнет — её можно спокойно решить без снятия всех ограничений и "разгона" сервера до бесконечности.
В большинстве случаев файл можно обрабатывать последовательно, небольшими кусками, например, тем же XMLReader — он позволяет идти по структуре XML, не загружая всё сразу в память. То есть out of memory можно избежать просто за счёт корректного подхода, а не за счёт бесконечных set_time_limit и memory_limit.
Очереди, транзакции и прочая "тяжёлая артиллерия" хороши, но применяются по мере необходимости. Если интеграция простая, объём данных умеренный, а обработка не критична к времени — можно обойтись без сложной архитектуры. А если нужна транзакционность — она легко добавляется в нужных местах, без нагромождения изначально.
P.S. Да, FTP-сервер — это просто сервер, где запущен протокол FTP (File Transfer Protocol) для обмена файлами.
vfork
а слово Bitrix как-нибудь убирается из ленты (1С тоже)?