Как быстро и правильно прочитать большой XML-файл в 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Работа с файлами обмена больших объемов (от 100 МБ до нескольких гигабайт) — классическая задача в практике разработчика 1С. При попытке прочитать огромный XML-документ «в лоб» через стандартную модель DOM или фабрику XDTO система часто аварийно завершает работу из-за нехватки оперативной памяти. В этой статье мы подробно разберем, как правильно организовать быстрое и стабильное чтение больших XML-файлов в 1С, рассмотрим основные архитектурные подходы, проанализируем примеры кода и разберем новые возможности платформы 1С:Предприятие.

Способ 1. Последовательное чтение через ЧтениеXML (SAX-подобный парсер)

Если вам необходимо прочитать сверхбольшой файл с минимальным потреблением оперативной памяти, стандартный объект платформы Чтение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" Тогда
            Возврат Категория;
        КонецЕсли;
    КонецЦикла;
    
    Возврат Неопределено;
    
КонецФункции

Преимущества метода: Работает максимально быстро, потребляет минимальное и фиксированное количество оперативной памяти независимо от размера исходного файла.

Недостатки метода: Требует написания объемного и сложного программного кода для ручного контроля вложенности элементов и переходов между узлами.

Способ 2. Чтение через XDTO и решение проблемы динамической типизации коллекций

Механизм XML-данных (XDTO) позволяет автоматически превращать XML-документ в дерево объектов 1С, с которым очень удобно работать в коде. Однако при чтении XML без жестко заданной XSD-схемы разработчики часто сталкиваются с логической ловушкой.

Проанализируем ситуацию: если у элемента XML есть вложенная коллекция (например, список категорий Category у товара Product), то:

  1. Если у товара содержится несколько категорий, фабрика XDTO прочитает свойство как коллекцию типа СписокXDTO.
  2. Если у товара содержится всего одна категория, фабрика XDTO прочитает ее как одиночный элемент типа ОбъектXDTO.
  3. Если категорий у товара нет вообще, свойство вернет значение Неопределено или пустую строку.

Попытка обойти такую динамическую коллекцию обычным циклом Для Каждого приведет к критической ошибке времени выполнения, если у товара окажется всего одна категория. Чтобы решить эту проблему раз и навсегда, напишем специальную функцию-обертку, которая будет гарантированно приводить любое полученное свойство к массиву.

Рассмотрим пример реализации чтения файла через 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.Свойства().Получить(ИмяСвойства) <> Неопределено;
КонецФункции

Способ 3. Гибридный метод «Потокового DOM» (Микро-DOM)

Существует элегантное и эффективное решение, совмещающее в себе экономичность последовательного чтения и удобство объектной модели 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.Закрыть();
    
КонецПроцедуры

Способ 4. Динамическое создание XSD-схемы для Фабрики XDTO

Чтобы избежать написания вспомогательных функций преобразования коллекций (как во втором способе), мы можем динамически сформировать простейшую 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.

Способ 5. Нарезка файлов и распараллеливание через фоновые задания

Когда речь идет о гигабайтных объемах данных, чтение в один поток упирается в производительность одного ядра процессора сервера. Платформа 1С позволяет эффективно распараллелить эту задачу. Схема реализации выглядит следующим образом:

  1. Главный поток быстро считывает файл как текстовый, определяя его размер в байтах.
  2. Файл разделяется на примерные равные части (например, по 50 МБ каждая).
  3. Для каждого блока главный поток корректирует смещение: сдвигает указатель вперед до ближайшего закрывающего тега </Product>, чтобы гарантировать целостность данных каждого товара внутри одного блока.
  4. Запускается пул ФоновыеЗадания. Каждому заданию передается путь к файлу, байтовое смещение начала блока и байтовое смещение конца блока.
  5. Внутри фонового задания файл открывается как поток, выполняется метод Перейти (позиционирование на нужный байт) и выполняется последовательный разбор только своей порции данных.

Такой подход на многопроцессорных серверах приложений позволяет ускорить парсинг XML-файлов в 3–4 раза. Для автоматизации переноса больших объемов данных частями можно использовать обработку универсального обмена с автоматическим разделением выгрузки на части.

Способ 6. Быстрое извлечение данных с помощью регулярных выражений (для платформ 8.3.22+)

Начиная с версии платформы 8.3.22, разработчикам доступен нативный, высокопроизводительный движок регулярных выражений на уровне ядра С++. Если перед вами стоит задача быстро получить из огромного файла плоский список идентификаторов, артикулов или цен без разбора сложной иерархии тегов, использование XML-анализаторов избыточно.

Проанализируем пример, как прочитать файл блоками как текст и быстро собрать все сопоставления ID продуктов и их названий с помощью регулярных выражений:


&НаСервере
Процедура ИзвлечьДанныеРегулярнымиВыражениями(ПутьКФайлу)
    
    // Открываем файл как текстовый для чтения потоком
    ТекстФайла = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
    
    // Шаблон регулярного выражения для поиска блоков товара
    ШаблонВыражения = "<ProductID>(\d+)</ProductID>\s*<ProductName>([^<]+)</ProductName>";
    
    // Считываем файл крупными порциями
    РазмерПорции = 1048576; // 1 МБ
    Пока Истина Цикл
        
        ЧастьТекста = ТекстФайла.Прочитать(РазмерПорции);
        Если ПустаяСтрока(ЧастьТекста) Тогда
            Прервать;
        КонецЕсли;
        
        // Используем встроенные функции регулярных выражений платформы
        РезультатыПоиска = СтрНайтиПоРегулярномуВыражению(ЧастьТекста, ШаблонВыражения, НаправлениеПоиска.Вперед, Истина);
        
        Для Каждого Результат Из РезультатыПоиска Цикл
            // Быстро получаем ИД товара и его наименование без инициализации объектов XML
            IDТовара  = Результат.SubMatches[0];
            ИмяТовара = Результат.SubMatches[1];
            // Обрабатываем полученную пару значений
        КонецЦикла;
        
    КонецЦикла;
    
    ТекстФайла.Закрыть();
    
КонецПроцедуры

Способ 7. Чтение и парсинг средствами СУБД (MS SQL Server / PostgreSQL)

Если сервер базы данных расположен на том же сервере или имеет сетевой доступ к 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-файлами:

← На главную