Как установить свойство URIПространстваИмен для ЭлементаDOM в 1С без перезаписи файлов

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

При интеграции систем и обмене данными по стандарту CommerceML разработчики часто сталкиваются с необходимостью разбора тяжелых файлов XML (размером в сотни мегабайт или даже гигабайты). Использование классического последовательного чтения XML (объект ЧтениеXML) не всегда удобно, когда требуется извлечь сложные структурированные данные. В таких сценариях на помощь приходит связка из технологии DOM и фабрики XDTO: данные извлекаются с помощью быстрых xPath-запросов, а затем преобразуются в типизированные объекты XDTO для удобной работы в коде.

Однако здесь разработчиков подстерегает неочевидная проблема. Элементы DOM, извлеченные из дерева документа, часто теряют контекст пространств имен. При попытке прочитать такой элемент с помощью метода ФабрикаXDTO.ПрочитатьXML() платформа выдает ошибку, так как для сопоставления узла с типом XDTO у объекта ЭлементDOM обязательно должно быть заполнено свойство URIПространстваИмен. Если оно равно Неопределено, фабрика XDTO не сможет распознать тип, даже если на корневом уровне документа пространство имен было объявлено.

В этой статье мы подробно разберем, почему стандартные попытки решения этой проблемы не работают, и рассмотрим три высокопроизводительных способа установки пространства имен в оперативной памяти без перезаписи тяжелых файлов на диск.

Почему не работают очевидные решения?

Для начала проанализируем типичные ошибки и попытки, которые часто совершают разработчики при работе с объектной моделью документа DOM в 1С.

  1. Использование метода УстановитьСоответствиеПространстваИмен. Этот метод создает текстовый атрибут соответствия пространства имен (например, xmlns="urn:1C.ru:commerceml_2") для конкретного узла. Однако он не перестраивает внутреннюю объектную структуру уже созданного дерева DOM в оперативной памяти. Свойство URIПространстваИмен у самого элемента и у всех его дочерних узлов по-прежнему остается равным Неопределено. Фабрика XDTO при чтении ориентируется именно на внутреннее свойство узла, а не на его текстовые атрибуты, поэтому чтение завершается ошибкой.
  2. Метод ПереименоватьУзел без рекурсии. Метод ПереименоватьУзел (DOMDocument.RenameNode) действительно умеет физически менять пространство имен узла в памяти. Но он делает это только для одного конкретного элемента, переданного в параметрах. Он не распространяет изменения на дочерние элементы. Если вы переименуете только корневой элемент извлеченной ветки, первый же вложенный реквизит (например, наименование или код товара) вызовет сбой при чтении через XDTO, так как его пространство имен останется пустым.
  3. Импорт и адаптация узлов (ИмпортироватьУзел / АдаптироватьУзел). По спецификации DOM, если узел-источник изначально не имел пространства имен, то при его импорте в новый DOM-документ (даже если у нового документа задан корневой URI) импортируемый узел не приобретает его автоматически. Он сохраняет свои исходные свойства, включая пустое пространство имен.

Запись всего XML-документа обратно на диск с добавлением пространства имен и его повторное чтение решает проблему, но для файлов объемом более 500 МБ этот костыль неприемлем из-за критического падения производительности дисковой подсистемы и процессора. Рассмотрим оптимальные альтернативы.

Решение 1. Рекурсивное переименование узлов в памяти (рекомендуемый способ)

Самый быстрый и производительный способ — выполнить обход полученной через XPath ветки DOM-дерева рекурсивно и принудительно присвоить каждому элементу нужное пространство имен с помощью метода ПереименоватьУзел. Поскольку все операции происходят со ссылками на объекты в оперативной памяти, этот процесс занимает миллисекунды даже на относительно больших ветках данных.

Создадим рекурсивную процедуру обхода элементов дерева DOM:


Процедура УстановитьПространствоИменУзлуРекурсивно(ДокументDOM, ТекущийУзел, URI) Export
    
    Если ТекущийУзел.ТипУзла = ТипУзлаDOM.Элемент Тогда
        
        // Переименовываем узел, явно сопоставляя его с новым URI пространства имен
        ДокументDOM.ПереименоватьУзел(ТекущийУзел, URI, ТекущийУзел.ИмяУзла);
        
        // Рекурсивно обрабатываем всех дочерних детей текущего элемента
        Для Каждого ДочернийУзел Из ТекущийУзел.ДочерниеУзлы Цикл
            УстановитьПространствоИменУзлуРекурсивно(ДокументDOM, ДочернийУзел, URI);
        КонецЦикла;
        
    КонецЕсли;
    
КонецПроцедуры

Теперь применим эту процедуру на практике для чтения объекта XDTO из извлеченного элемента DOM:


// Предположим, мы получили элемент DOM (например, одну конкретную Группу из файла CommerceML)
ЭлементDOM = ДокументDOM.ПервыйДочерний; 
URI_CML = "urn:1C.ru:commerceml_2";

// Запускаем рекурсивное переименование всей извлеченной ветки в ОЗУ
УстановитьПространствоИменУзлуРекурсивно(ДокументDOM, ЭлементDOM, URI_CML);

// Теперь открываем чтение узлов DOM. Объектная модель полностью готова
ЧтениеУзловDOM = Новый ЧтениеУзловDOM;
ЧтениеУзловDOM.Открыть(ЭлементDOM);

Попытка
    // Фабрика XDTO успешно прочитает типизированный объект, так как у всех узлов заполнен URIПространстваИмен
    ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеУзловDOM, ФабрикаXDTO.Тип(URI_CML, "Группа"));
    
    // Выводим результат для проверки
    Сообщить("Успешно прочитан объект XDTO: " + ОбъектXDTO.Наименование);
Исключение
    Сообщить("Ошибка чтения XDTO: " + ОписаниеОшибки());
КонецПопытки;

Преимущества метода: Абсолютное отсутствие операций ввода-вывода (I/O). Память не забивается дубликатами строк XML, все модификации производятся "на лету" прямо в исходном дереве DOM.

Решение 2. Локальная сериализация ветки XML в оперативную память (ОЗУ)

Если по какой-то причине вы не хотите обходить дерево рекурсивно (например, при экстремально глубокой вложенности структуры XML, которая теоретически может вызвать переполнение стека), можно применить сериализацию. Но важно делать это не во временный файл на диске, а непосредственно в ОЗУ.

Поскольку XPath-запрос возвращает не весь 500-мегабайтный документ, а лишь маленькую целевую ветку, её сериализация в память займет ничтожно мало времени.

Вариант А. Сериализация в XML-строку (простой метод)

Этот способ отлично подходит для небольших и средних по объему веток XML.


URI_CML = "urn:1C.ru:commerceml_2";

// 1. Принудительно устанавливаем xmlns на уровне корневого элемента извлекаемой ветки
ЭлементDOM.УстановитьСоответствиеПространстваИмен("", URI_CML);

// 2. Записываем только этот локальный элемент в строку XML (в оперативную память)
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку(); // Перенаправляем вывод в ОЗУ

ЗаписьDOM = Новый ЗаписьDOM;
ЗаписьDOM.Записать(ЭлементDOM, ЗаписьXML);

СтрокаXMLДанных = ЗаписьXML.Закрыть();

// 3. Читаем подготовленную строку через ЧтениеXML. 
// При парсинге строки парсер автоматически проставит всем дочерним узлам нужное пространство имен
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(СтрокаXMLДанных);

Попытка
    ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML, ФабрикаXDTO.Тип(URI_CML, "Группа"));
    Сообщить("Объект успешно импортирован из строки!");
Исключение
    Сообщить("Ошибка импорта: " + ОписаниеОшибки());
КонецПопытки;

Вариант Б. Сериализация через потоки в памяти (для крупных веток)

Если извлекаемая ветка сама по себе достаточно объемная (содержит тысячи вложенных элементов), операции со строками в 1С могут вызывать фрагментацию памяти. Начиная с версии платформы 8.3.9, мы можем использовать эффективные низкоуровневые потоки в памяти.


URI_CML = "urn:1C.ru:commerceml_2";
ЭлементDOM.УстановитьСоответствиеПространстваИмен("", URI_CML);

// Создаем поток в оперативной памяти
ПотокВПамяти = Новый ПотокВПамяти();

ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьПоток(ПотокВПамяти);

// Записываем ветку DOM в поток
ЗаписьDOM = Новый ЗаписьDOM;
ЗаписьDOM.Записать(ЭлементDOM, ЗаписьXML);
ЗаписьXML.Закрыть(); // Сбрасываем буферы записи в поток

// Возвращаем указатель потока в начало (позиция 0) для последующего чтения
ПотокВПамяти.Перейти(0, ПозицияВПотоке.Начало);

// Читаем данные из потока в памяти
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьПоток(ПотокВПамяти);

Попытка
    ОбъектXDTO = ФабрикаXDTO.ПрочитатьXML(ЧтениеXML, ФабрикаXDTO.Тип(URI_CML, "Группа"));
Исключение
    Сообщить("Ошибка при чтении из потока: " + ОписаниеОшибки());
КонецПопытки;

// Закрываем поток для освобождения памяти
ПотокВПамяти.Закрыть();

Альтернативный вариант: Загрузка схемы XSD без целевого пространства имен

Существует еще один концептуальный обходной путь. Если исходный XML-файл CommerceML принципиально не содержит пространств имен, а вы хотите использовать сильные стороны XDTO (типизацию данных), вы можете динамически модифицировать схему пакета XSD перед созданием фабрики.

Для этого выполняются следующие шаги:

  1. Текст схемы XSD считывается программно.
  2. Из схемы удаляется атрибут targetNamespace.
  3. Схема загружается в фабрику XDTO динамически.

В результате создается локальная фабрика XDTO, которая ожидает типы данных вообще без пространства имен (с пустым URI). Это позволяет читать элементы DOM напрямую методом ФабрикаXDTO.ПрочитатьXML(ЧтениеУзловDOM, ФабрикаXDTO.Тип("", "Группа")) без каких-либо манипуляций с пространствами имен в самом документе XML. Однако этот способ сложнее в реализации, так как требует поддержки динамических схем и написания парсера XSD.

Резюме и рекомендации

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

← На главную