Работа с внешними файлами через механизмы COM-объектов — это классическая задача для разработчика 1С, но она таит в себе множество подводных камней. Одной из самых раздражающих проблем является внезапное «замирание» программы. Вы запускаете отладку, доходите до строки инициализации объекта, и на этом всё — 1С перестает отвечать, а выполнение кода не продолжается. В этой статье мы подробно разберем, почему так происходит, как диагностировать проблему и какими способами можно оптимизировать чтение данных из Excel.
Когда мы вызываем функцию ПолучитьCOMОбъект(ИмяФайла), операционная система пытается открыть файл с помощью приложения, зарегистрированного в реестре (в данном случае — Microsoft Excel). Однако Excel — это полноценное интерактивное приложение, которое «любит» общаться с пользователем через диалоговые окна.
Рассмотрим ситуацию: вы программно открываете файл, а Excel в этот момент хочет сообщить вам, что файл скачан из интернета и открыт в защищенном просмотре, или что в нем есть неработающие связи, или (что бывает чаще всего) предложить восстановить документ после предыдущего аварийного завершения. Поскольку при программном вызове Excel часто запускается в невидимом режиме, это модальное окно «зависает» в памяти. Система ждет нажатия кнопки пользователем, но пользователь этого окна не видит. В итоге 1С бесконечно ожидает ответа от COM-сервера.
Для диагностики этой проблемы в первую очередь выполним совет из обсуждения: принудительно включим видимость приложения. Это позволит нам увидеть, что именно «спрашивает» Excel. Разберем, как изменить подход к инициализации:
// Вместо неявного получения объекта через файл
// Эксель = ПолучитьCOMОбъект(ИмяФайла);
// Используем явное создание приложения
ЭксельПриложение = Новый COMОбъект("Excel.Application");
ЭксельПриложение.Visible = Истина; // Теперь мы увидим все окна и вопросы
Попытка
Книга = ЭксельПриложение.Workbooks.Open(ИмяФайла);
Исключение
Сообщить("Не удалось открыть файл: " + ОписаниеОшибки());
ЭксельПриложение.Quit();
Возврат;
КонецПопытки;
Проанализируем еще одну критическую ошибку, упомянутую в теме: накопление множества процессов excel.exe в диспетчере задач. Автор обнаружил у себя 9 запущенных копий Excel. Это происходит из-за того, что в 1С объект COM не уничтожается автоматически сразу после завершения процедуры, если на него или его дочерние элементы (листы, ячейки, области) остались ссылки.
Давайте разберем по шагам, как правильно завершать сеанс работы с Excel, чтобы не перегружать сервер или рабочую станцию:
Close() для открытой книги.Quit() для самого объекта приложения.Неопределено в обратном порядке: сначала ячейки и области, затем листы, затем книги и в самом конце — само приложение.Посмотрим на правильный паттерн завершения работы:
// ... код обработки данных ...
// Важно закрыть книгу без сохранения изменений
Книга.Close(Ложь);
// Завершаем работу приложения
ЭксельПриложение.Quit();
// Освобождаем память
Лист = Неопределено;
Книга = Неопределено;
ЭксельПриложение = Неопределено;
Чтобы код работал стабильно в автоматическом режиме (например, в фоновом задании на сервере), нам нужно запретить Excel выводить любые вопросы. Для этого проанализируем свойства DisplayAlerts и AskToUpdateLinks. Добавим их в наш алгоритм:
Эксель = Новый COMОбъект("Excel.Application");
// Отключаем вывод предупреждений
Эксель.DisplayAlerts = Ложь;
// Отключаем запрос на обновление связей при открытии
Эксель.AskToUpdateLinks = Ложь;
// Открываем книгу в режиме "только чтение", чтобы избежать блокировок
Книга = Эксель.Workbooks.Open(ИмяФайла, 0, Истина);
В исходном коде автора используется метод SpecialCells(11) для определения границ данных. Это решение рабочее, но не всегда точное, так как Excel часто хранит информацию о форматировании пустых строк как о «использованных». Кроме того, чтение каждой ячейки в цикле ЛистЭксель.Cells(r, c).Value — это очень медленная операция, так как каждое обращение — это отдельный вызов через COM-интерфейс.
Рассмотрим гораздо более быстрый способ — выгрузку всей области данных в массив (COMSafeArray) одним разом. Это сокращает время обработки в десятки раз:
// Определяем область данных
ВсегоСтрок = ЛистЭксель.Cells.SpecialCells(11).Row;
ВсегоКолонок = ЛистЭксель.Cells.SpecialCells(11).Column;
// Получаем все данные одним массивом
Область = ЛистЭксель.Range(ЛистЭксель.Cells(1, 1), ЛистЭксель.Cells(ВсегоСтрок, ВсегоКолонок));
ДанныеМассив = Область.Value; // Это вернет двумерный массив
// Теперь работаем с массивом в памяти 1С, что в разы быстрее
Для СтрокаЦ = 1 По ВсегоСтрок Цикл
Для КолонкаЦ = 1 По ВсегоКолонок Цикл
ЗначениеЯчейки = ДанныеМассив.GetValue(КолонкаЦ, СтрокаЦ);
// Обработка значения...
КонецЦикла;
КонецЦикла;
В коде автора используется MSScriptControl.ScriptControl с языком javascript для получения текущего времени в миллисекундах. Важно понимать, что этот компонент является 32-битным. Если ваша 1С запущена в 64-битной версии (что сейчас является стандартом для серверов), этот код вызовет ошибку «Класс не зарегистрирован».
Давайте выясним, как сделать замер времени более современным и кроссплатформенным способом. Начиная с версии платформы 8.3.10, в 1С доступна встроенная функция:
ВремяНачала = ТекущаяУниверсальнаяДатаВМиллисекундах();
// ... ваш код выполнения ...
ВремяОкончания = ТекущаяУниверсальнаяДатаВМиллисекундах();
Сообщить("Затрачено времени (мс): " + (ВремяОкончания - ВремяНачала));
Это избавляет нас от зависимости от сторонних COM-компонентов и работает значительно быстрее.
Использование COM-объекта Excel оправдано, когда вам нужно сложное форматирование, печать или запуск макросов внутри файла. Если же ваша задача — просто прочитать данные, проанализируем более стабильные варианты:
ТабДок.Прочитать(ПутьКФайлу). Он работает без установленного Microsoft Office, очень быстр и стабилен на сервере (в том числе на Linux)..xlsx напрямую как ТабличныйДокумент, что является наиболее рекомендуемым способом для импорта данных.Резюмируем: Если вы столкнулись с «зависанием» на ПолучитьCOMОбъект, первым делом проверьте диспетчер задач на наличие «лишних» процессов Excel и попробуйте включить видимость приложения (Visible = Истина), чтобы увидеть скрытые диалоговые окна. И не забывайте всегда вызывать Quit() для корректного завершения работы приложения.