Как исправить ошибку «Обращение к процедуре объекта как к функции» при чтении XML в 1С?

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

При написании обработок интеграции и загрузки данных из внешних источников разработчики 1С часто сталкиваются с чтением файлов формата XML. В процессе работы с объектом ЧтениеXML нередко возникает досадная синтаксическая ошибка: «Обращение к процедуре объекта как к функции». Чаще всего она указывает на строку инициализации файла, например, на вызов метода ОткрытьФайл.

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

Выясняем причину: Разница между процедурой и функцией в 1С

Для начала разберем природу возникшей ошибки. В платформе 1С:Предприятие все методы встроенного языка четко разделены на два типа:

  1. Процедуры — выполняют определенный алгоритм действий, но не возвращают никакого значения в точку вызова. Их нельзя присваивать переменным.
  2. Функции — выполняют расчет или чтение данных и обязательно возвращают результат с помощью оператора Возврат. Результат работы функции можно (и нужно) присвоить переменной или использовать в выражении.

Если мы обратимся к Синтакс-Помощнику 1С для изучения объекта ЧтениеXML, то увидим следующее описание метода ОткрытьФайл:

ЧтениеXML.ОткрытьФайл (XMLReader.OpenFile)
Сигнатура: ОткрытьФайл(<Путь>, <ПараметрыЧтенияXML>)
Тип метода: Процедура.

Поскольку этот метод является процедурой, он не возвращает открытый поток или сам XML-документ. Он лишь инициализирует внутреннее состояние самого объекта ЧтениеXML, связывая его с указанным файлом на диске.

Посмотрим на ошибочный код, с которого началась тема на форуме:


ЧтениеXML = Новый ЧтениеXML;
Файл_XML = ЧтениеXML.ОткрытьФайл("D:\НоваяПапка\document.xml"); // Ошибка здесь!

Попытка присвоить результат выполнения процедуры переменной Файл_XML приводит к моментальному аварийному завершению программы с сообщением: «Обращение к процедуре объекта как к функции (ОткрытьФайл)». Платформа ожидает, что справа от знака равенства находится функция, но встречает процедуру.

Правильный синтаксис вызова выглядит чрезвычайно просто — без какого-либо присваивания:


ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл("D:\НоваяПапка\document.xml"); // Правильно!

После этого объект ЧтениеXML готов к последовательному чтению данных. Сам открытый файл «находится» внутри этого объекта.

Разбор скрытых ошибок «готового решения» с форума

Очень часто на форумах авторы закрывают ветки обсуждений радостным сообщением «Все заработало, спасибо!», предоставляя код, который на самом деле содержит критические уязвимости. Если скопировать такое решение в рабочую конфигурацию, можно получить неработающий механизм, дублирование данных или утечки оперативной памяти. Проанализируем эти скрытые проблемы подробнее.

1. Несуществующий метод Найти() у менеджера справочника

В финальных сообщениях темы участники предлагают использовать следующую конструкцию:


НайденныйКонтрагент = Справочники.Контрагенты.Найти(Число(ЧтениеXML.ЗначениеАтрибута("УНП")), "УНП");

В чем критическая ошибка: У объекта типа СправочникМенеджер (в данном случае Справочники.Контрагенты) нет метода с именем Найти! Попытка выполнения этой строки вызовет ошибку времени выполнения: «Метод объекта не обнаружен (Найти)».

Для поиска элементов по реквизиту (в нашем случае по УНП) необходимо использовать строго предназначенный для этого метод НайтиПоРеквизиту:


НайденныйКонтрагент = Справочники.Контрагенты. НайтиПоРеквизиту("УНП", ИскомоеЗначениеУНП);

2. Логическая ловушка сравнения с Неопределено

В коде автора фигурирует проверка:


Если НайденныйКонтрагент <> Неопределено Тогда 
    Продолжить; 
КонецЕсли;

В чем проблема: Методы поиска элементов справочников (такие как НайтиПоРеквизиту, НайтиПоКоду, НайтиПоНаименованию) в случае неуспешного поиска никогда не возвращают значение Неопределено. Если элемент не найден, эти методы возвращают пустую ссылку соответствующего типа (то есть СправочникСсылка.Контрагенты).

Поскольку пустая ссылка не равна значению Неопределено, условие НайденныйКонтрагент <> Неопределено всегда будет принимать истинное значение (Истина). В результате программа будет постоянно заходить в ветку Продолжить, и новые контрагенты вообще никогда не создадутся! Либо, если условие перевернуто некорректно, база заполнится дублирующимися записями.

Правильная проверка на успешность поиска выглядит так:


Если ЗначениеЗаполнено(НайденныйКонтрагент) Тогда
    // Контрагент уже существует в базе, пропускаем создание
    Продолжить;
КонецЕсли;

Или альтернативный вариант с явной проверкой на пустую ссылку:


Если Не НайденныйКонтрагент.Пустая() Тогда
    Продолжить;
КонецЕсли;

3. Отсутствие обязательного закрытия потока (блокировка файлов)

Ни в одном сообщении на форуме не упоминается метод Закрыть() для объекта ЧтениеXML.

Чем это грозит: Если вы открыли файл методом ОткрытьФайл, прочитали его и не вызвали в конце метод Закрыть(), операционная система будет считать этот файл заблокированным процессом 1С (службой сервера или клиентским сеансом). При попытке повторного запуска регламентного задания или при попытке перезаписать этот XML-файл сторонней программой, вы гарантированно получите ошибку совместного доступа к файлу (File Access Denied).

Очистка памяти в 1С происходит автоматически («сборщик мусора»), но полагаться на нее в работе с файловыми потоками категорически нельзя. Поток нужно закрывать явно, причем в блоке Попытка — Исключение, чтобы гарантировать закрытие файла даже при возникновении фатальных сбоев.

4. Специфика выполнения в регламентных заданиях на сервере

Автор ветки упомянул, что код вызывается из регламентного (фонового) задания. В серверном контексте есть две важные особенности:


Проектируем правильное решение: Вариант с последовательным ЧтениемXML

Рассмотрим детально, как должен выглядеть надежный алгоритм импорта данных через последовательный обход XML-документа. Нам важно учитывать структуру XML. Представим, что у нас файл имеет структуру, где данные контрагента лежат внутри тега <Контрагент> в виде вложенных элементов:


<?xml version="1.0" encoding="UTF-8"?>
<Данные>
    <Контрагент>
        <УНП>123456789</УНП>
        <Наименование>ООО Новые Технологии</Наименование>
        <ПолноеНаименование>Общество с ограниченной ответственностью Новые Технологии</ПолноеНаименование>
    </Контрагент>
</Данные>

Напишем процедуру разбора такой структуры с использованием последовательного чтения:


Процедура ЗаписатьДанныеНаСервере() Экспорт

    ПутьКФайлу = "D:\НоваяПапка\document.xml";
    
    // Проверим физическое существование файла перед началом работы
    ФайлНаДиске = Новый Файл(ПутьКФайлу);
    Если Не ФайлНаДиске.Существует() Тогда
        ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентов", УровеньЖурналаРегистрации.Ошибка, , , 
            "Файл не найден по пути: " + ПутьКФайлу);
        Возврат;
    КонецЕсли;

    ЧтениеXML = Новый ЧтениеXML;
    
    Попытка
        ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
    Исключение
        ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентов", УровеньЖурналаРегистрации.Ошибка, , , 
            "Не удалось открыть XML-файл. Ошибка: " + ОписаниеОшибки());
        Возврат;
    КонецПопытки;

    // Инициализируем переменные для сбора данных текущего контрагента
    ТекущийУНП = 0;
    ТекущееНаименование = "";
    ТекущееПолноеНаименование = "";

    // Начинаем последовательное чтение документа
    Попытка
        Пока ЧтениеXML.Прочитать() Цикл
            
            // Нам интересны только начала элементов (тегов)
            Если ЧтениеXML.ТипУзла = ТипУзлаXML.НачалоЭлемента Тогда
                
                ИмяТега = ЧтениеXML.Имя;
                
                Если ИмяТега = "Контрагент" Тогда
                    // Сбрасываем переменные для нового контрагента
                    ТекущийУНП = 0;
                    ТекущееНаименование = "";
                    ТекущееПолноеНаименование = "";
                    
                ИначеЕсли ИмяТега = "УНП" Тогда
                    // Переходим к текстовому содержимому внутри тега УНП
                    ЧтениеXML.Прочитать();
                    Если ЧтениеXML.ТипУзла = ТипУзлаXML.Текст Тогда
                        ТекущийУНП = Число(СокрЛП(ЧтениеXML.Значение));
                    КонецЕсли;
                    
                ИначеЕсли ИмяТега = "Наименование" Тогда
                    ЧтениеXML.Прочитать();
                    Если ЧтениеXML.ТипУзла = ТипУзлаXML.Текст Тогда
                        ТекущееНаименование = СокрЛП(ЧтениеXML.Значение);
                    КонецЕсли;
                    
                ИначеЕсли ИмяТега = "ПолноеНаименование" Тогда
                    ЧтениеXML.Прочитать();
                    Если ЧтениеXML.ТипУзла = ТипУзлаXML.Текст Тогда
                        ТекущееПолноеНаименование = СокрЛП(ЧтениеXML.Значение);
                    КонецЕсли;
                КонецЕсли;
                
            // Если дошли до закрывающего тега "Контрагент", записываем накопленные данные в БД
            ИначеЕсли ЧтениеXML.ТипУзла = ТипУзлаXML.КонецЭлемента И ЧтениеXML.Имя = "Контрагент" Тогда
                
                Если ТекущийУНП = 0 Тогда
                    ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентов", УровеньЖурналаРегистрации.Предупреждение, , , 
                        "Пропущен контрагент без УНП: " + ТекущееНаименование);
                    Продолжить;
                КонецЕсли;
                
                // Выполняем поиск по реквизиту (метод НайтиПоРеквизиту!)
                НайденныйКонтрагент = Справочники.Контрагенты.НайтиПоРеквизиту("УНП", ТекущийУНП);
                
                // Проверяем на заполненность (не на Неопределено!)
                Если ЗначениеЗаполнено(НайденныйКонтрагент) Тогда
                    // Контрагент уже есть в системе, пропускаем создание
                    Продолжить;
                КонецЕсли;
                
                // Создаем и заполняем новый элемент справочника
                НовыйКонтрагент = Справочники.Контрагенты.СоздатьЭлемент();
                НовыйКонтрагент.Наименование       = ТекущееНаименование;
                НовыйКонтрагент.ПолноеНаименование = ТекущееПолноеНаименование;
                НовыйКонтрагент.УНП                = ТекущийУНП;
                
                Попытка
                    НовыйКонтрагент.Записать();
                Исключение
                    ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентов", УровеньЖурналаРегистрации.Ошибка, , , 
                        "Не удалось записать контрагента УНП " + ТекущийУНП + ". Ошибка: " + ОписаниеОшибки());
                КонецПопытки;
                
            КонецЕсли;
            
        КонецЦикла;
        
        ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентов", УровеньЖурналаРегистрации.Информация, , , 
            "Загрузка контрагентов успешно завершена.");
            
    Исключение
        ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентов", УровеньЖурналаРегистрации.Ошибка, , , 
            "Критическая ошибка при чтении XML. Описание: " + ОписаниеОшибки());
    КонецПопытки;
    
    // Блок закрытия файла гарантирует освобождение ресурсов в ОС
    ЧтениеXML.Закрыть();

КонецПроцедуры

Альтернативное решение: Использование Объектной Модели Документа (DOM)

Для файлов небольшого и среднего размера (до 10–20 Мегабайт) последовательный разбор тегов через ЧтениеXML может показаться избыточно сложным и громоздким. Намного удобнее загрузить XML-документ целиком в оперативную память в виде дерева объектов DOM (Document Object Model). Это позволит обращаться к узлам по их именам напрямую.

Разберем, как реализовать этот подход на практике с помощью встроенных объектов платформы ПостроительDOM и ДокументDOM:


Процедура ЗагрузитьКонтрагентовЧерезDOM() Экспорт

    ПутьКФайлу = "D:\НоваяПапка\document.xml";
    
    ЧтениеXML = Новый ЧтениеXML;
    Попытка
        ЧтениеXML.ОткрытьФайл(ПутьКФайлу);
    Исключение
        ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентовDOM", УровеньЖурналаРегистрации.Ошибка, , , 
            "Ошибка открытия файла: " + ОписаниеОшибки());
        Возврат;
    КонецПопытки;
    
    // Читаем XML файл в структуру DOM
    ПостроительDOM = Новый ПостроительDOM;
    ДокументDOM = ПостроительDOM.Прочитать(ЧтениеXML);
    
    // Закрываем ЧтениеXML сразу после построения DOM-модели, освобождая файл!
    ЧтениеXML.Закрыть();
    
    // Получаем список всех узлов с тегом "Контрагент"
    СписокУзловКонтрагентов = ДокументDOM.ПолучитьЭлементыПоИмени("Контрагент");
    
    Для каждого УзелКонтрагента Из СписокУзловКонтрагентов Цикл
        
        // Получаем значения дочерних элементов
        ТекущийУНП = 0;
        ТекущееНаименование = "";
        ТекущееПолноеНаименование = "";
        
        Для каждого ДочернийУзел Из УзелКонтрагента.ДочерниеУзлы Цикл
            
            Если ДочернийУзел.ТипУзла = ТипУзлаDOM.Элемент Тогда
                
                ИмяУзла = ДочернийУзел.ИмяУзла;
                ЗначениеТекста = СокрЛП(ДочернийУзел.ТекстовоеСодержимое);
                
                Если ИмяУзла = "УНП" Тогда
                    ТекущийУНП = Число(ЗначениеТекста);
                ИначеЕсли ИмяУзла = "Наименование" Тогда
                    ТекущееНаименование = ЗначениеТекста;
                ИначеЕсли ИмяУзла = "ПолноеНаименование" Тогда
                    ТекущееПолноеНаименование = ЗначениеТекста;
                КонецЕсли;
                
            КонецЕсли;
            
        КонецЦикла;
        
        // Проверяем корректность данных
        Если ТекущийУНП = 0 Тогда
            Продолжить;
        КонецЕсли;
        
        // Поиск в справочнике
        НайденныйКонтрагент = Справочники.Контрагенты.НайтиПоРеквизиту("УНП", ТекущийУНП);
        Если ЗначениеЗаполнено(НайденныйКонтрагент) Тогда
            Продолжить; // Контрагент уже существует
        КонецЕсли;
        
        // Создание нового контрагента
        НовыйКонтрагент = Справочники.Контрагенты.СоздатьЭлемент();
        НовыйКонтрагент.УНП                = ТекущийУНП;
        НовыйКонтрагент.Наименование       = ТекущееНаименование;
        НовыйКонтрагент.ПолноеНаименование = ТекущееПолноеНаименование;
        
        Попытка
            НовыйКонтрагент.Записать();
        Исключение
            ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентовDOM", УровеньЖурналаРегистрации.Ошибка, , , 
                "Не удалось сохранить контрагента " + ТекущееНаименование + ": " + ОписаниеОшибки());
        КонецПопытки;
        
    КонецЦикла;
    
    ЗаписьЖурналаРегистрации("ЗагрузкаКонтрагентовDOM", УровеньЖурналаРегистрации.Информация, , , 
        "Загрузка через DOM успешно завершена.");

КонецПроцедуры

Резюме по правильной работе с XML

Для того чтобы избежать ошибок при загрузке данных из XML-файлов, всегда следуйте золотым правилам разработки 1С:

← На главную