Почему при чтении CSV через ADODB в 1С возвращаются одинаковые строки и как это исправить?

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

Чтение структурированных текстовых файлов и прайс-листов через технологию COM и провайдер OLE DB — классический прием интеграции в среде 1С:Предприятие под Windows. Однако при работе с набором записей ADODB.Recordset разработчики нередко сталкиваются с ситуацией, когда цикл перебора отрабатывает нужное количество итераций, но на каждом шаге результирующая таблица заполняется абсолютно идентичными данными из первой строки.

Разберем причины такого поведения, выясним, почему метод Recordset.MoveNext() кажется неработающим, и рассмотрим правильные варианты реализации обмена данными.

В чем кроется основная причина ошибки

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


ПоляСтроки = Recordset.Fields;
СтрокаПрайсов.Наименование = ПоляСтроки.Item(Число(ПараметрыИмпорта.ТегНаименование)).Name;

Основная ошибка заключается в обращении к свойству Name вместо свойства Value. Свойство Name возвращает имя колонки (заголовка), сформированное драйвером при открытии набора записей. Естественно, имя колонки остается неизменным на протяжении всего файла, сколько бы раз мы ни вызывали Recordset.MoveNext(). В результате в коллекцию раз за разом записывалось строковое наименование поля, создавая иллюзию «залипания» курсора на первой строке.

Кроме того, в подобных обработчиках часто присутствуют сопутствующие ошибки:

  1. Пропуск шага курсора перед переходом к следующей итерации: при использовании оператора Продолжить в условных конструкциях (например, при фильтрации по цене) вызов Recordset.MoveNext() опускается, что приводит к бесконечному зацикливанию алгоритма, если условие сработает.
  2. Неявное определение типов данных: по умолчанию драйвер ACE OLEDB анализирует только первые несколько строк файла (параметр TypeGuessRows). Если в колонке с числовым кодом внезапно появляется строка, драйвер может возвращать пустое значение (NULL / Неопределено).
  3. Избыточное создание объектов: инициализация ADODB.Command для простого запроса к CSV-файлу избыточна — запрос можно выполнять напрямую методом Execute() у объекта соединения ADODB.Connection.

Исправление ошибки и настройка файла schema.ini

Для гарантированного считывания данных без искажения типов и с точным разделением колонок сформируем файл конфигурации schema.ini в той же папке, где находится файл CSV. Этот файл сообщает OLE DB драйверу разделитель, наличие заголовков и явные типы данных каждого поля.

Рассмотрим рабочий вариант загрузки данных через ADODB с явным указанием схемы и корректным обращением к Value:


ФайлПрайса = Новый Файл(ЛокальныйПутьКФайлу);

СтрокаСоединения = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + ФайлПрайса.Путь + ";Extended Properties='Text;HDR=No;FMT=Delimited';";

// Формируем файл схемы для однозначного сопоставления колонок
ФорматДелимитер = Новый ТекстовыйДокумент();
ФорматДелимитер.ДобавитьСтроку("[" + ФайлПрайса.Имя + "]");
ФорматДелимитер.ДобавитьСтроку("Format=Delimited(;)");
ФорматДелимитер.ДобавитьСтроку("ColNameHeader=False");
ФорматДелимитер.ДобавитьСтроку("Col" + Строка(Число(ПараметрыИмпорта.ТегШтрихкод) + 1) + "=Barcode Text");
ФорматДелимитер.ДобавитьСтроку("Col" + Строка(Число(ПараметрыИмпорта.ТегНаименование) + 1) + "=Name Text");
ФорматДелимитер.ДобавитьСтроку("Col" + Строка(Число(ПараметрыИмпорта.ТегРозничнаяЦена) + 1) + "=Price Currency");
ФорматДелимитер.ДобавитьСтроку("Col" + Строка(Число(ПараметрыИмпорта.ТегКоличество) + 1) + "=Qty Text");
ФорматДелимитер.Записать(ФайлПрайса.Путь + "schema.ini", КодировкаТекста.ANSI);    

Connection = Новый COMОбъект("ADODB.Connection");
Connection.ConnectionString = СтрокаСоединения;

Попытка
    Connection.Open();
Исключение
    ВызватьИсключение "Не удалось подключиться к источнику данных: " + ОписаниеОшибки();
КонецПопытки;    

ТекстЗапроса = "SELECT * FROM [" + ФайлПрайса.Имя + "]";
RecordSet = Connection.Execute(ТекстЗапроса);

ПоляСтроки = RecordSet.Fields;

Пока Не RecordSet.EOF Цикл        
    
    ЦенаРозничная = Число(ПоляСтроки.Item("Price").Value);
    Если ПараметрыИмпорта.ОкруглятьВМеньшуюСторону Тогда
        ЦенаРозничная = Ценообразование.ОкруглитьЦену(Цел(ЦенаРозничная), ПараметрыИмпорта.ПорядокОкругления, Ложь);
    Иначе
        ЦенаРозничная = Ценообразование.ОкруглитьЦену(ЦенаРозничная, ПараметрыИмпорта.ПорядокОкругления, Ложь);
    КонецЕсли;
    
    // Обязательно двигаем курсор перед оператором Продолжить!
    Если ПараметрыИмпорта.НеЗагружатьРозничныеЦеныМеньше > ЦенаРозничная Тогда
        RecordSet.MoveNext();        
        Продолжить;
    КонецЕсли;
    
    СтрокаПрайсов = ТаблицаПрайсов.Добавить();
    СтрокаПрайсов.Штрихкод = ПараметрыИмпорта.ДобавлятьПрефиксШтрихкода + СокрЛП(Строка(ПоляСтроки.Item("Barcode").Value));
    СтрокаПрайсов.Наименование = Строка(ПоляСтроки.Item("Name").Value);
    СтрокаПрайсов.ЦенаЗакупочная = ЦенаРозничная * ПараметрыИмпорта.КоэффициентЗакупочнойЦены;
    СтрокаПрайсов.ЦенаРозничная = ЦенаРозничная;
    
    Если Число(ПоляСтроки.Item("Qty").Value) > 0 Тогда
        СтрокаПрайсов.Количество = ПараметрыИмпорта.КоличествоПоУмолчанию;
    Иначе
        СтрокаПрайсов.Количество = 0;
    КонецЕсли;

    RecordSet.MoveNext();        
КонецЦикла;

RecordSet.Close();
Connection.Close();
RecordSet = Неопределено;
Connection = Неопределено;     

// Очищаем временный файл схемы
УдалитьФайлы(ФайлПрайса.Путь + "schema.ini");

Ограничения использования ADODB в современных конфигурациях

Хотя чтение через ADODB решает поставленную задачу, у этого подхода есть существенные технические ограничения, о которых необходимо помнить:

  1. Несовместимость с Linux: COM-объекты являются частью операционной системы Windows. На сервере 1С под управлением Linux данный код завершится критической ошибкой.
  2. Несоответствие разрядностей (32 / 64 бит): провайдер Microsoft.ACE.OLEDB.12.0 должен строго совпадать по разрядности с процессом платформы 1С. Если 64-разрядный сервер 1С обращается к 32-разрядному драйверу ACE OLEDB, возникнет ошибка инициализации COM-объекта.
  3. Конфликты schema.ini при параллельной работе: драйвер считывает файл конфигурации из каталога файла данных. Если несколько пользователей или фоновых заданий одновременно загружают файлы в общую папку, перезапись schema.ini вызовет состояние гонки (race condition) и повреждение структуры данных.
  4. Производительность построчного обхода: каждое обращение к свойствам COM-объекта в цикле порождает накладные расходы на межпроцессное взаимодействие (marshaling). Для ускорения больших выборок предпочтительнее использовать выгрузку массива через метод RecordSet.GetRows().

Альтернатива: штатный объект 1С «ЧтениеCSV»

Начиная с версии платформы 8.3.10, в 1С встроен специализированный объект ЧтениеCSV, полностью избавляющий разработчика от необходимости использовать внешние OLE DB драйверы, следить за разрядностью и зависеть от Windows.

Посмотрим на реализацию той же задачи средствами платформы:


ПараметрыРазбора = Новый ПараметрыЧтенияCSV();
ПараметрыРазбора.РазделительПолей = ";";
ПараметрыРазбора.СимволЭкранирования = """";

ЧтениеФайла = Новый ЧтениеCSV();
ЧтениеФайла.Открыть(ЛокальныйПутьКФайлу, ПараметрыРазбора);

ИндексШтрихкод = Число(ПараметрыИмпорта.ТегШтрихкод);
ИндексНаименование = Число(ПараметрыИмпорта.ТегНаименование);
ИндексЦена = Число(ПараметрыИмпорта.ТегРозничнаяЦена);
ИндексКоличество = Число(ПараметрыИмпорта.ТегКоличество);

Пока ЧтениеФайла.Прочитать() Цикл
    
    // Получаем текущую строку как массив значений колонок
    Колонки = ЧтениеФайла.ПолучитьЗначения();
    
    Если Колонки.Количество() <= ИндексЦена Тогда
        Продолжить;
    КонецЕсли;
    
    ЦенаРозничная = Число(Колонки[ИндексЦена]);
    Если ПараметрыИмпорта.ОкруглятьВМеньшуюСторону Тогда
        ЦенаРозничная = Ценообразование.ОкруглитьЦену(Цел(ЦенаРозничная), ПараметрыИмпорта.ПорядокОкругления, Ложь);
    Иначе
        ЦенаРозничная = Ценообразование.ОкруглитьЦену(ЦенаРозничная, ПараметрыИмпорта.ПорядокОкругления, Ложь);
    КонецЕсли;
    
    Если ПараметрыИмпорта.НеЗагружатьРозничныеЦеныМеньше > ЦенаРозничная Тогда
        Продолжить;
    КонецЕсли;
    
    СтрокаПрайсов = ТаблицаПрайсов.Добавить();
    СтрокаПрайсов.Штрихкод = ПараметрыИмпорта.ДобавлятьПрефиксШтрихкода + СокрЛП(Колонки[ИндексШтрихкод]);
    СтрокаПрайсов.Наименование = Колонки[ИндексНаименование];
    СтрокаПрайсов.ЦенаЗакупочная = ЦенаРозничная * ПараметрыИмпорта.КоэффициентЗакупочнойЦены;
    СтрокаПрайсов.ЦенаРозничная = ЦенаРозничная;
    
    Количество = 0;
    Если Колонки.Количество() > ИндексКоличество И Число(Колонки[ИндексКоличество]) > 0 Тогда
        Количество = ПараметрыИмпорта.КоличествоПоУмолчанию;
    КонецЕсли;
    СтрокаПрайсов.Количество = Количество;
    
КонецЦикла;

ЧтениеФайла.Закрыть();

Переход на объект ЧтениеCSV гарантирует кроссплатформенность, исключает блокировки файловой системы из-за незакрытых COM-соединений и обеспечивает стабильно высокую скорость парсинга файлов любого объема.

← На главную