Работа с файлами обмена больших объемов (от 100 МБ до нескольких гигабайт) — классическая задача в практике разработчика 1С. При попытке прочитать огромный XML-документ «в лоб» через стандартную модель DOM или фабрику XDTO система часто аварийно завершает работу из-за нехватки оперативной памяти. В этой статье мы подробно разберем, как правильно организовать быстрое и стабильное чтение больших XML-файлов в 1С, рассмотрим основные архитектурные подходы, проанализируем примеры кода и разберем новые возможности платформы 1С:Предприятие.
Если вам необходимо прочитать сверхбольшой файл с минимальным потреблением оперативной памяти, стандартный объект платформы ЧтениеXML — это базовый и самый надежный инструмент. Он работает по принципу SAX-парсера: последовательно перемещает указатель по файлу от начала к концу, считывая за один шаг только один узел. При этом в памяти не удерживается вся структура документа.
Рассмотрим пример реализации последовательного чтения для структуры данных, где присутствуют товары и их категории. Проанализируем ситуацию, когда нам необходимо обойти дерево тегов без загрузки всей иерархии в память:
&НаСервере
Процедура ПрочитатьXMLПоследовательно(ПутьКФайлу)
Чтение = Новый ЧтениеXML;
Попытка
Чтение.ОткрытьФайл(ПутьКФайлу);
Исключение
ЗаписьЖурналаРегистрации("Чтение XML", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
Возврат;
КонецПопытки;
ТекущийТовар = Неопределено;
Пока Чтение.Прочитать() Цикл
// Проверяем начало элемента
Если Чтение.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
ИмяУзла = Чтение.ЛокальноеИмя;
Если ИмяУзла = "Product" Тогда
// Создаем структуру для накопления данных о товаре
ТекущийТовар = Новый Структура("ID, Name, Categories");
ТекущийТовар.Categories = Новый Массив;
ИначеЕсли ИмяУзла = "ProductID" Тогда
Чтение.Прочитать(); // Переходим к текстовому содержимому
Если Чтение.ТипУзла = ТипУзлаXML.Текст Тогда
ТекущийТовар.ID = Чтение.Значение;
КонецЕсли;
ИначеЕсли ИмяУзла = "ProductName" Тогда
Чтение.Прочитать();
Если Чтение.ТипУзла = ТипУзлаXML.Текст Тогда
ТекущийТовар.Name = Чтение.Значение;
КонецЕсли;
ИначеЕсли ИмяУзла = "Category" Тогда
// Читаем вложенные свойства категории
СтруктураКатегории = ПрочитатьУзелКатегории(Чтение);
Если СтруктураКатегории <> Неопределено Тогда
ТекущийТовар.Categories.Добавить(СтруктураКатегории);
КонецЕсли;
КонецЕсли;
// Проверяем конец элемента товара, чтобы записать его в базу
ИначеЕсли Чтение.ТипУзла = ТипУзлаXML.КонецЭлемента И Чтение.ЛокальноеИмя = "Product" Тогда
// Здесь выполняем обработку полученного товара
ОбработатьПолученныйТовар(ТекущийТовар);
ТекущийТовар = Неопределено;
КонецЕсли;
КонецЦикла;
Чтение.Закрыть();
КонецПроцедуры
&НаСервере
Функция ПрочитатьУзелКатегории(Чтение)
Категория = Новый Структура("CategoryID, CategoryName");
Пока Чтение.Прочитать() Цикл
Если Чтение.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
ИмяТега = Чтение.ЛокальноеИмя;
Чтение.Прочитать();
Если Чтение.ТипУзла = ТипУзлаXML.Текст Тогда
Если ИмяТега = "CategoryID" Тогда
Категория.CategoryID = Чтение.Значение;
ИначеЕсли ИмяТега = "CategoryName" Тогда
Категория.CategoryName = Чтение.Значение;
КонецЕсли;
КонецЕсли;
ИначеЕсли Чтение.ТипУзла = ТипУзлаXML.КонецЭлемента И Чтение.ЛокальноеИмя = "Category" Тогда
Возврат Категория;
КонецЕсли;
КонецЦикла;
Возврат Неопределено;
КонецФункции
Преимущества метода: Работает максимально быстро, потребляет минимальное и фиксированное количество оперативной памяти независимо от размера исходного файла.
Недостатки метода: Требует написания объемного и сложного программного кода для ручного контроля вложенности элементов и переходов между узлами.
Механизм XML-данных (XDTO) позволяет автоматически превращать XML-документ в дерево объектов 1С, с которым очень удобно работать в коде. Однако при чтении XML без жестко заданной XSD-схемы разработчики часто сталкиваются с логической ловушкой.
Проанализируем ситуацию: если у элемента XML есть вложенная коллекция (например, список категорий Category у товара Product), то:
СписокXDTO.ОбъектXDTO.Неопределено или пустую строку.Попытка обойти такую динамическую коллекцию обычным циклом Для Каждого приведет к критической ошибке времени выполнения, если у товара окажется всего одна категория. Чтобы решить эту проблему раз и навсегда, напишем специальную функцию-обертку, которая будет гарантированно приводить любое полученное свойство к массиву.
Рассмотрим пример реализации чтения файла через XDTO с обработкой динамических коллекций:
&НаСервере
Процедура ПрочитатьXMLЧерезXDTO(ПутьКФайлу)
Чтение = Новый ЧтениеXML;
Чтение.ОткрытьФайл(ПутьКФайлу);
// Читаем XML без использования предопределенной схемы XSD
Попытка
ДанныеФайла = ФабрикаXDTO.ПрочитатьXML(Чтение);
Исключение
Предупреждение("Не удалось прочитать XML-файл через XDTO!");
Чтение.Закрыть();
Возврат;
КонецПопытки;
// Предположим, корневой тег - Products, содержащий список Product
Для Каждого ТоварXDTO Из ПолучитьМассивИзКоллекции(ДанныеФайла.Product) Цикл
ИмяТовара = ТоварXDTO.ProductName;
IDТовара = ТоварXDTO.ProductID;
// Обрабатываем вложенный список категорий
Если СвойствоСуществует(ТоварXDTO, "Categories") И ТоварXDTO.Categories <> Неопределено Тогда
Для Каждого КатегорияXDTO Из ПолучитьМассивИзКоллекции(ТоварXDTO.Categories.Category) Цикл
IDКатегории = КатегорияXDTO.CategoryID;
ИмяКатегории = КатегорияXDTO.CategoryName;
// Обработка данных категории
КонецЦикла;
КонецЕсли;
КонецЦикла;
Чтение.Закрыть();
КонецПроцедуры
&НаСервере
Функция ПолучитьМассивИзКоллекции(Знач ОбъектXDTO)
Если ОбъектXDTO = Неопределено Тогда
Возврат Новый Массив;
КонецЕсли;
// Если тип значения является списком XDTO - возвращаем его напрямую
Если ТипЗнч(ОбъектXDTO) = Тип("СписокXDTO") Тогда
Возврат ОбъектXDTO;
КонецЕсли;
// Если это одиночный объект - оборачиваем его в стандартный массив 1С
МассивОбъектов = Новый Массив;
МассивОбъектов.Добавить(ОбъектXDTO);
Возврат МассивОбъектов;
КонецФункции
&НаСервере
Функция СвойствоСуществует(ОбъектXDTO, ИмяСвойства)
Возврат ОбъектXDTO.Свойства().Получить(ИмяСвойства) <> Неопределено;
КонецФункции
Существует элегантное и эффективное решение, совмещающее в себе экономичность последовательного чтения и удобство объектной модели DOM. Его суть заключается в следующем: мы используем быстрое ЧтениеXML, чтобы пропустить служебные шаги и дойти до интересующего нас крупного узла (например, <Product>). Как только указатель встает на этот узел, мы передаем объект чтения в ПостроительDOM.
Построитель DOM считывает из потока только один конкретный узел со всей его вложенной структурой и превращает его в компактный документ DOM, с которым очень удобно работать. После обработки этого узла оперативная память мгновенно очищается, а указатель ЧтениеXML двигается дальше к следующему элементу.
Разберем этот метод на практике:
&НаСервере
Процедура ПрочитатьГибриднымМетодом(ПутьКФайлу)
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
ПостроительDOM = Новый ПостроительDOM;
// Позиционируемся на элементах
Пока ЧтениеXML.Прочитать() Цикл
Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента И ЧтениеXML.ЛокальноеИмя = "Product" Тогда
// Считываем только один узел Product в локальную DOM-модель
ДокументDOM = ПостроительDOM.Прочитать(ЧтениеXML);
// Получаем корневой элемент этого микро-документа
ЭлементТовар = ДокументDOM.ДокументЭлемент;
// Теперь мы можем использовать удобные методы поиска DOM внутри этого узла
СписокИмен = ЭлементТовар.ПолучитьЭлементыПоИмени("ProductName");
Если СписокИмен.Количество() > 0 Тогда
ИмяТовара = СписокИмен[0].ТекстовоеСодержимое;
КонецЕсли;
// Быстро вытаскиваем вложенные категории
СписокКатегорий = ЭлементТовар.ПолучитьЭлементыПоИмени("Category");
Для Каждого ЭлементКатегория Из СписокКатегорий Цикл
IDКатегории = "";
СписокID = ЭлементКатегория.ПолучитьЭлементыПоИмени("CategoryID");
Если СписокID.Количество() > 0 Тогда
IDКатегории = СписокID[0].ТекстовоеСодержимое;
КонецЕсли;
// Дальнейшая обработка...
КонецЦикла;
КонецЕсли;
КонецЦикла;
ЧтениеXML.Закрыть();
КонецПроцедуры
Чтобы избежать написания вспомогательных функций преобразования коллекций (как во втором способе), мы можем динамически сформировать простейшую XSD-схему и инициализировать на ее основе фабрику XDTO. Это гарантирует, что даже одиночные элементы всегда будут считываться как списки (коллекции).
Рассмотрим, как сгенерировать схему XSD прямо в оперативной памяти и использовать ее при парсинге:
&НаСервере
Функция ПолучитьЛокальнуюФабрикуXDTO()
// Формируем текст XSD-схемы, жестко определяющий тип Category как список (maxOccurs="unbounded")
ТекстСхемы =
"<?xml version=""1.0"" encoding=""UTF-8""?>
|<xs:schema xmlns:xs=""http://www.w3.org/2001/XMLSchema"" elementFormDefault=""qualified"">
| <xs:element name=""Products"">
| <xs:complexType>
| <xs:sequence>
| <xs:element name=""Product"" maxOccurs=""unbounded"">
| <xs:complexType>
| <xs:sequence>
| <xs:element name=""ProductName"" type=""xs:string""/>
| <xs:element name=""ProductID"" type=""xs:integer""/>
| <xs:element name=""Categories"" minOccurs=""0"">
| <xs:complexType>
| <xs:sequence>
| <xs:element name=""Category"" maxOccurs=""unbounded"">
| <xs:complexType>
| <xs:sequence>
| <xs:element name=""CategoryID"" type=""xs:integer""/>
| <xs:element name=""CategoryName"" type=""xs:string""/>
| </xs:sequence>
| </xs:complexType>
| </xs:element>
| </xs:sequence>
| </xs:complexType>
| </xs:element>
| </xs:sequence>
| </xs:complexType>
| </xs:element>
| </xs:sequence>
| </xs:complexType>
| </xs:element>
|</xs:schema>";
ЧтениеСхемы = Новый ЧтениеXML;
ЧтениеСхемы.УстановитьСтроку(ТекстСхемы);
НаборСхем = Новый НаборСхемXML;
НаборСхем.Добавить(ЧтениеСхемы);
// Создаем локальную фабрику XDTO на основе нашей динамической схемы
ЛокальнаяФабрика = Новый ФабрикаXDTO(НаборСхем);
Возврат ЛокальнаяФабрика;
КонецФункции
Используя созданную таким образом ЛокальнаяФабрика, свойство Product.Categories.Category всегда будет иметь тип СписокXDTO независимо от того, сколько категорий содержится в XML-файле у конкретного товара (ноль, одна или сто). Для подготовки готовых моделей из схем сторонних сервисов можно использовать инструмент для конвертации схемы XSD в модель XDTO.
Когда речь идет о гигабайтных объемах данных, чтение в один поток упирается в производительность одного ядра процессора сервера. Платформа 1С позволяет эффективно распараллелить эту задачу. Схема реализации выглядит следующим образом:
</Product>, чтобы гарантировать целостность данных каждого товара внутри одного блока.ФоновыеЗадания. Каждому заданию передается путь к файлу, байтовое смещение начала блока и байтовое смещение конца блока.Перейти (позиционирование на нужный байт) и выполняется последовательный разбор только своей порции данных.Такой подход на многопроцессорных серверах приложений позволяет ускорить парсинг XML-файлов в 3–4 раза. Для автоматизации переноса больших объемов данных частями можно использовать обработку универсального обмена с автоматическим разделением выгрузки на части.
Начиная с версии платформы 8.3.22, разработчикам доступен нативный, высокопроизводительный движок регулярных выражений на уровне ядра С++. Если перед вами стоит задача быстро получить из огромного файла плоский список идентификаторов, артикулов или цен без разбора сложной иерархии тегов, использование XML-анализаторов избыточно.
Проанализируем пример, как прочитать файл блоками как текст и быстро собрать все сопоставления ID продуктов и их названий с помощью регулярных выражений:
&НаСервере
Процедура ИзвлечьДанныеРегулярнымиВыражениями(ПутьКФайлу)
// Открываем файл как текстовый для чтения потоком
ТекстФайла = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
// Шаблон регулярного выражения для поиска блоков товара
ШаблонВыражения = "<ProductID>(\d+)</ProductID>\s*<ProductName>([^<]+)</ProductName>";
// Считываем файл крупными порциями
РазмерПорции = 1048576; // 1 МБ
Пока Истина Цикл
ЧастьТекста = ТекстФайла.Прочитать(РазмерПорции);
Если ПустаяСтрока(ЧастьТекста) Тогда
Прервать;
КонецЕсли;
// Используем встроенные функции регулярных выражений платформы
РезультатыПоиска = СтрНайтиПоРегулярномуВыражению(ЧастьТекста, ШаблонВыражения, НаправлениеПоиска.Вперед, Истина);
Для Каждого Результат Из РезультатыПоиска Цикл
// Быстро получаем ИД товара и его наименование без инициализации объектов XML
IDТовара = Результат.SubMatches[0];
ИмяТовара = Результат.SubMatches[1];
// Обрабатываем полученную пару значений
КонецЦикла;
КонецЦикла;
ТекстФайла.Закрыть();
КонецПроцедуры
Если сервер базы данных расположен на том же сервере или имеет сетевой доступ к XML-файлу, самым быстрым способом обработки будет перекладывание парсинга на СУБД. Например, в MS SQL Server встроен мощный процессор XML на основе XQuery.
Рассмотрим, как это работает на уровне прямого SQL-запроса:
SELECT
Product.value('(ProductID)[1]', 'INT') AS ProductID,
Product.value('(ProductName)[1]', 'VARCHAR(255)') AS ProductName
FROM
(SELECT CAST(BulkColumn AS XML) FROM OPENROWSET(BULK 'C:\Shared\products.xml', SINGLE_BLOB) AS x) AS T(x)
OUTER APPLY
x.nodes('/Products/Product') AS Products(Product);
Как запустить это в 1С: Разработчик может подключиться к СУБД через механизм внешних источников данных (ВИД) или использовать прямое ADO-подключение. СУБД разберет файл на уровне ядра базы данных в разы быстрее, чем это сделает любой код на встроенном языке 1С. Полученный результат можно сразу записать во временную таблицу базы данных и работать с ней далее через стандартные запросы 1С.
Подводя итог, выделим ключевые правила выбора инструмента для работы с большими XML-файлами: