Часто разработчики сталкиваются с задачей импорта данных из объемных файлов формата CSV (с миллионами строк). Стандартный подход с построчным чтением через ТекстовыйДокумент занимает слишком много времени и неоправданно расходует ресурсы сервера. Разберем по шагам, как можно оптимизировать этот процесс и загрузить огромный файл целиком, быстро формируя готовую ТаблицаЗначений.
Проанализируем одно из самых быстрых решений, доступных "из коробки" в среде Windows — использование COM-объектов ADO. Данный метод позволяет обратиться к текстовому файлу с разделителями с помощью SQL-запроса, как к обычной таблице базы данных. Скорость чтения при этом существенно возрастает.
Посмотрим на пример клиент-серверного кода, который реализует этот подход. Сначала мы выбираем файл, а затем читаем его содержимое через драйвер Microsoft Text Driver, выгружая данные напрямую в массив и далее в таблицу значений.
&НаКлиенте
Процедура ПрочитатьCSV(Команда)
Режим = РежимДиалогаВыбораФайла.Открытие;
ДиалогОткрытияФайла = Новый ДиалогВыбораФайла(Режим);
ДиалогОткрытияФайла.Фильтр = "Текстовый документ(*.txt;*.csv;)|*.txt;*.csv;";
ДиалогОткрытияФайла.МножественныйВыбор = Ложь;
Если ДиалогОткрытияФайла.Выбрать() Тогда
ВыбФайл = Новый Файл(ДиалогОткрытияФайла.ВыбранныеФайлы[0]);
Connection = Новый ComОбъект("ADODB.Connection");
Command = Новый ComОбъект("ADODB.Command");
// Указываем строку подключения к папке с файлом
СтрокаПодключения = "Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq=" + ВыбФайл.Путь + ";Extensions=csv;";
Connection.ConnectionString = СтрокаПодключения;
Connection.Open();
Command.ActiveConnection = Connection;
// Обращаемся к файлу с помощью SQL-запроса
Command.CommandText = "SELECT * FROM [" + ВыбФайл.Имя + "]";
Recordset = Command.Execute();
Колонки = Новый Массив;
Для Каждого Кол Из Recordset.Fields Цикл
Колонки.Добавить(Кол.Name);
КонецЦикла;
// Передаем массив колонок и выгруженные данные на сервер
ПрочитатьCSVНаСервере(Колонки, Recordset.GetRows().Выгрузить());
КонецЕсли;
КонецПроцедуры
&НаСервере
Процедура ПрочитатьCSVНаСервере(Колонки, МассивСтрок)
Таб = Новый ТаблицаЗначений;
// Формируем колонки таблицы значений
Для Инд = 0 По Колонки.ВГраница() Цикл
Типы = Новый Массив;
// Обход для определения типов колонок по первой строке
НовыйТип = ТипЗнч(МассивСтрок[0][Инд]);
Типы.Добавить(НовыйТип);
Таб.Колонки.Добавить(Колонки[Инд], Новый ОписаниеТипов(Типы));
КонецЦикла;
// Заполняем таблицу значений данными
Для Каждого Строка Из МассивСтрок Цикл
НовСтр = Таб.Добавить();
Для Инд = 0 По Колонки.ВГраница() Цикл
НовСтр[Колонки[Инд]] = Строка[Инд];
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Выясним причину, почему разработчики жалуются на низкую скорость стандартного чтения. Проблема кроется в использовании объекта ТекстовыйДокумент. При вызове метода Прочитать() платформа пытается загрузить весь гигантский файл в оперативную память для построения объектной модели документа, а последующие вызовы ПолучитьСтроку() работают крайне медленно.
Рассмотрим правильный нативный способ потокового чтения с помощью объекта ЧтениеТекста. Этот объект не выгружает весь файл в память, а читает его последовательно, строка за строкой. Данный подход позволяет обрабатывать файлы размером в несколько гигабайт за считанные минуты, не вызывая нехватку памяти (Out of Memory).
Чтение = Новый ЧтениеТекста(ПутьКФайлу, КодировкаТекста.UTF8);
СтрокаCSV = Чтение.ПрочитатьСтроку();
Пока СтрокаCSV <> Неопределено Цикл
// Разбор строки по разделителю
// ... логика парсинга ...
СтрокаCSV = Чтение.ПрочитатьСтроку();
КонецЦикла;
Чтение.Закрыть();
Проанализируем еще один встроенный механизм. В современных версиях платформы 1С объект ТабличныйДокумент научился напрямую открывать текстовые файлы с разделителями. Метод Прочитать() позволяет передать параметры форматирования, включая кодировку и символ-разделитель.
После успешного чтения мы можем использовать ПостроительЗапроса или ПостроительОтчета для быстрой конвертации области табличного документа в объект ТаблицаЗначений. Однако будьте осторожны: для файлов из миллионов строк этот метод не подходит, так как построение визуальной сетки табличного документа в памяти сервера израсходует все доступные ресурсы.
Если ваша информационная база развернута на MS SQL Server или PostgreSQL, рассмотрим самый быстрый из существующих способов — загрузку на уровне самой СУБД. Мы можем полностью исключить сервер 1С из процесса первоначального парсинга файла.
COMОбъект("ADODB.Connection") отправляем серверу БД команду массовой загрузки: BULK INSERT (для MS SQL) или COPY (для PostgreSQL). СУБД "заглотит" миллионы строк за несколько секунд.ВнешниеИсточникиДанных.Начиная с версии 8.3, в платформе появились низкоуровневые объекты Поток и ЧтениеДанных. Разберем их преимущества. Они позволяют считывать файл бинарными блоками (чанками). Это открывает возможность для распараллеливания задачи.
Мы можем запустить несколько фоновых заданий: первое задание читает первые 100 Мб файла, второе — следующие 100 Мб и так далее. Для гигантских интеграций это позволяет сократить время обработки пропорционально количеству свободных ядер на сервере 1С.
Поскольку встроенный язык 1С является интерпретируемым, операции со строками и массивами в огромных циклах всегда будут уступать по скорости компилируемым языкам. Разберем альтернативу: сообществом разработчиков создано множество внешних компонент (на C++, C#, Rust) специально для парсинга CSV (например, FastCSV).
Подключив такую компоненту по технологии Native API, мы просто передаем ей путь к нашему файлу на диске. Компонента самостоятельно парсит текст в многопоточном режиме и возвращает в 1С уже заполненную объектную ТаблицаЗначений. Это избавляет программиста от необходимости писать сложные алгоритмы обхода колонок.
Часто иллюзия «медленного чтения CSV» возникает вовсе не из-за парсинга файла, а из-за медленной записи объектов в базу. Если внутри цикла чтения файла сразу идет создание элементов справочника СправочникМенеджер или записей регистров счетов, скорость будет катастрофически падать с каждой новой строкой.
Запомните важное правило: обработку больших файлов обязательно нужно оборачивать в транзакции порциями.
НачатьТранзакцию();
Счетчик = 0;
// Цикл чтения строк
Пока СтрокаCSV <> Неопределено Цикл
// Запись данных в базу
Счетчик = Счетчик + 1;
Если Счетчик % 1000 = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
СтрокаCSV = Чтение.ПрочитатьСтроку();
КонецЦикла;
ЗафиксироватьТранзакцию();
Запись порциями по 1000–5000 строк в десятки раз снижает нагрузку на дисковую подсистему СУБД, уменьшает рост журнала транзакций и кардинально ускоряет процесс импорта данных.