Как разбить таблицу значений или выгрузить данные в файлы порциями по 100 строк в 1С?

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

При выгрузке данных из 1С во внешние системы (например, при обмене данными с сайтами, отправке отчетов или интеграции через API) часто возникает ограничение на объем передаваемой информации. Одним из наиболее частых требований является жесткое ограничение на количество строк в одном файле выгрузки — например, не более 100 строк на один файл CSV или TXT.

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

Анализ подходов и критические ошибки производительности

Начинающие разработчики часто пытаются решить задачу «в лоб», совершая ошибки, которые незаметны на тестовых базах из 10–50 строк, но приводят к фатальному падению производительности на реальных объемах данных (от 10 000 строк и более). Рассмотрим типичные заблуждения.

Ошибка №1: Использование функции СтрПолучитьСтроку() в цикле для больших текстов.
Одним из предложенных на форуме решений было сначала сформировать одну гигантскую многострочную строку со всеми данными, а затем обходить ее в цикле, получая строки по одной через СтрПолучитьСтроку(Текст, НомерСтроки).

Выясним причину, почему это делать категорически нельзя. Функция СтрПолучитьСтроку работает не по индексу, как массив. Чтобы получить, к примеру, 5000-ю строку, платформа 1С вынуждена каждый раз сканировать весь текст с самого начала, отсчитывая символы перевода строк (Символы.ПС). При обходе текста из $N$ строк общая сложность алгоритма возрастает до квадратичной — $O(N^2)$. На таблице в 50 000 строк этот алгоритм будет выполняться часами, полностью заблокировав один из рабочих процессов сервера.

Ошибка №2: Использование объекта ТекстовыйДокумент вместо ЗаписьТекста.
Объект ТекстовыйДокумент предназначен в основном для интерактивной работы пользователя на форме (отображение, редактирование текста). Он полностью удерживает весь массив данных в оперативной памяти и производит много лишних ресурсоемких операций при вызове метода ДобавитьСтроку(). Для чисто программной, фоновой или потоковой записи файлов на диск следует всегда применять специализированный объект ЗаписьТекста, который работает через буферизованный поток напрямую с файловой системой и практически не потребляет оперативную память.

---

Вариант №1: Оптимальная потоковая выгрузка данных в файлы «на лету»

Если ваша задача заключается исключительно в том, чтобы взять результат запроса (или готовую таблицу значений) и сохранить его в файлы CSV по 100 строк в каждом, то самым производительным решением будет обход строк таблицы в цикле с одновременной записью в файл с помощью ЗаписьТекста. Нам не нужно предварительно делить саму таблицу значений в памяти или формировать огромные промежуточные строки.

Рассмотрим этот алгоритм по шагам. Мы организуем цикл по строкам таблицы значений. Будем вести счетчик строк для текущего файла. Как только счетчик достигает лимита (100 строк), мы закрываем текущий файл, инкрементируем индекс в имени файла, открываем новый файл и сбрасываем счетчик в ноль.

Посмотрим на пример практической реализации этого метода:


Процедура ВыгрузитьДанныеВПорцииCSV(ТаблицаДанных, ПутьККаталогу, БазовоеИмяФайла)
    
    // Определяем настройки выгрузки
    Разделитель = ";";
    ЛимитСтрок = 100;
    Кодировка = КодировкаТекста.ANSI; // Для корректного открытия в Excel без дополнительных настроек
    
    ТекущийИндексФайла = 1;
    СчетчикСтрокВТекущемФайле = 0;
    ЗаписьФайла = Неопределено;
    
    // Создаем структуру заголовка (первая строка CSV-файла с именами колонок)
    СтрокаЗаголовков = "";
    Для Каждого Колонка Из ТаблицаДанных.Колонки Цикл
        СтрокаЗаголовков = СтрокаЗаголовков + ?(ПустаяСтрока(СтрокаЗаголовков), "", Разделитель) + Колонка.Имя;
    КонецЦикла;
    
    Попытка
        Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
            
            // Если файл еще не открыт или достигнут лимит строк - инициализируем новый файл
            Если ЗаписьФайла = Неопределено Или СчетчикСтрокВТекущемФайле >= ЛимитСтрок Тогда
                
                // Если предыдущий файл был открыт - закрываем его перед созданием нового
                Если ЗаписьФайла <> Неопределено Тогда
                    ЗаписьФайла.Закрыть();
                    ЗаписьФайла = Неопределено;
                КонецЕсли;
                
                // Формируем уникальное имя файла, например: Данные_1.csv, Данные_2.csv
                ПолноеИмяФайла = ПутьККаталогу + "\" + БазовоеИмяФайла + "_" + Формат(ТекущийИндексФайла, "ЧГ=") + ".csv";
                ЗаписьФайла = Новый ЗаписьФекста(ПолноеИмяФайла, Кодировка);
                
                // Записываем шапку таблицы в новый файл
                ЗаписьФайла.ЗаписатьСтроку(СтрокаЗаголовков);
                
                ТекущийИндексФайла = ТекущийИндексФайла + 1;
                СчетчикСтрокВТекущемФайле = 0;
                
            КонецЕсли;
            
            // Формируем строку данных со специальным форматированием значений
            СтрокаCSV = "";
            Для Каждого Колонка Из ТаблицаДанных.Колонки Цикл
                ЗначениеПоля = СтрокаДанных[Колонка.Имя];
                
                // Предотвращаем появление пробелов-разделителей разрядов в числах (например, "1 000" -> "1000")
                Если ТипЗнч(ЗначениеПоля) = Тип("Число") Тогда
                    ТекстЗначения = Формат(ЗначениеПоля, "ЧГ=");
                ИначеЕсли ТипЗнч(ЗначениеПоля) = Тип("Дата") Тогда
                    ТекстЗначения = Формат(ЗначениеПоля, "ДФ=dd.MM.yyyy");
                Иначе
                    ТекстЗначения = Строка(ЗначениеПоля);
                    
                    // Экранирование символов разделителя и кавычек для формата CSV
                    Если Найти(ТекстЗначения, Разделитель) > 0 Или Найти(ТекстЗначения, """") > 0 Или Найти(ТекстЗначения, Символы.ПС) > 0 Тогда
                        ТекстЗначения = StrReplace(ТекстЗначения, """", """""");
                        ТекстЗначения = """" + ТекстЗначения + """";
                    КонецЕсли;
                КонецЕсли;
                
                СтрокаCSV = СтрокаCSV + ?(ПустаяСтрока(СтрокаCSV), "", Разделитель) + ТекстЗначения;
            КонецЦикла;
            
            // Записываем строку в буфер файла
            ЗаписьФайла.ЗаписатьСтроку(СтрокаCSV);
            СчетчикСтрокВТекущемФайле = СчетчикСтрокВТекущемФайле + 1;
            
        КонецЦикла;
    Исключение
        // Обязательно гарантируем закрытие файла при возникновении ошибок
        Если ЗаписьФайла <> Неопределено Тогда
            ЗаписьФайла.Закрыть();
        КонецЕсли;
        ВызватьИсключение;
    КонецПопытки;
    
    // Закрываем последний открытый файл после окончания обхода таблицы
    Если ЗаписьФайла <> Неопределено Тогда
        ЗаписьФайла.Закрыть();
    КонецЕсли;
    
    Сообщить("Успешно выгружено файлов: " + (ТекущийИндексФайла - 1));

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

Преимущества этого решения:

  1. Высокая скорость работы: Данные пишутся сразу в файл без промежуточных тяжелых преобразований.
  2. Минимальный расход памяти: В оперативной памяти единовременно хранится только одна строка таблицы, что позволяет обрабатывать миллионы записей без угрозы переполнения памяти (ошибки "Недостаточно памяти").
  3. Экранирование спецсимволов: Реализована базовая поддержка спецификации CSV (обработка кавычек и разделителей внутри текста), что предотвратит "расползание" таблицы при открытии файлов в Microsoft Excel.

---

Вариант №2: Быстрое разбиение исходной Таблицы Значений в памяти платформы

Иногда логика бизнес-процесса требует не просто записать файлы на диск, а разделить исходную ТаблицаЗначений на массив более мелких таблиц (например, для последующей передачи этих порций в фоновые задания, отправки пакетами через web-сервисы или асинхронного выполнения сложных расчетов в несколько потоков).

При разбиении таблицы значений в памяти избегайте построчного добавления элементов через НоваяСтрока() и последующего ручного заполнения каждого реквизита в цикле. Платформа 1С предоставляет мощный встроенный инструмент — метод Скопировать() у объекта ТаблицаЗначений, который принимает массив строк в качестве параметра.

Давайте проанализируем ситуацию: метод Скопировать(МассивСтрок) выполняется на уровне скомпилированного С++ кода платформы. Он мгновенно воссоздает идентичную структуру колонок (включая типы данных и индексы) и копирует туда только указанные строки.

Разберем по шагам функцию разбиения таблицы значений на порции:


// Функция принимает исходную таблицу значений и размер желаемой порции.
// Возвращает Массив, содержащий новые таблицы значений меньшего размера.
Функция РазбитьТаблицуЗначенийНаПорции(ИсходнаяТЗ, РазмерПорции) Экспорт
    
    МассивРезультат = Новый Массив;
    ВсегоСтрок = ИсходнаяТЗ.Количество();
    ТекущийИндекс = 0;
    
    Пока ТекущийИндекс < ВсегоСтрок Цикл
        
        // Создаем временный массив для накопления строк текущей порции
        МассивСтрокДляКопирования = Новый Массив;
        
        // Вычисляем верхнюю границу текущего пакета, чтобы не выйти за пределы таблицы
        ИндексГраницыПорции = Мин(ТекущийИндекс + РазмерПорции, ВсегоСтрок);
        
        // Наполняем массив ссылками на строки исходной таблицы значений
        Для ИндексСтроки = ТекущийИндекс По ИндексГраницыПорции - 1 Цикл
            МассивСтрокДляКопирования.Добавить(ИсходнаяТЗ[ИндексСтроки]);
        КонецЦикла;
        
        // Быстрое копирование структуры и данных на уровне платформы
        НоваяПорцияТЗ = ИсходнаяТЗ.Скопировать(МассивСтрокДляКопирования);
        МассивРезультат.Добавить(НоваяПорцияТЗ);
        
        // Смещаем указатель на начало следующей порции
        ТекущийИндекс = ТекущийИндекс + РазмерПорции;
        
    КонецЦикла;
    
    Возврат МассивРезультат;
    
КонецФункции

После получения массива мелких таблиц значений, мы можем легко обойти их в цикле и передать в любую обрабатывающую функцию. Рассмотрим пример интеграции этой функции с выгрузкой файлов:


Процедура ВыполнитьРазбиениеИЗапись()
    
    // Шаг 1: Получаем исходную таблицу данных из запроса
    ТаблицаЗначений = ПолучитьТаблицуИзЗапроса();
    
    // Шаг 2: Разбиваем таблицу значений в памяти на части по 100 строк
    МассивТаблиц = РазбитьТаблицуЗначенийНаПорции(ТаблицаЗначений, 100);
    
    // Шаг 3: Записываем каждую порцию в отдельный файл
    НомерФайла = 1;
    Для Каждого ПорцияТЗ Из МассивТаблиц Цикл
        
        ИмяФайла = "C:\Выгрузка\Порция_" + Формат(НомерФайла, "ЧГ=") + ".csv";
        
        // Здесь мы можем использовать стандартный ТекстовыйДокумент,
        // так как объем данных гарантированно мал (всего 100 строк)
        ТекстовыйДок = Новый ТекстовыйДокумент;
        
        Для Каждого СтрокаТЗ Из ПорцияТЗ Цикл
            СтрокаДляЗаписи = Строка(СтрокаТЗ.Код) + ";" + Строка(СтрокаТЗ.Наименование);
            ТекстовыйДок.ДобавитьСтроку(СтрокаДляЗаписи);
        КонецЦикла;
        
        ТекстовыйДок.Записать(ИмяФайла, КодировкаТекста.ANSI);
        НомерФайла = НомерФайла + 1;
        
    КонецЦикла;
    
    Сообщить("Выгрузка завершена успешно!");
    
КонецПроцедуры

---

Важные нюансы при работе с кодировками файлов в 1С

При выгрузке текстовых данных крайне важно обращать внимание на кодировку результирующего файла:

Применяя описанные методики, вы гарантируете стабильную, быструю работу ваших интеграционных механизмов и исключаете появление неконтролируемых утечек памяти на сервере 1С.

← На главную