Выгрузка табличных данных в текстовый формат с разделителями (чаще всего CSV с разделителем «точка с запятой») — одна из самых частых задач в практике разработчика 1С. Такой формат обмена незаменим при интеграции со старыми информационными системами, импорте данных в сторонние веб-сервисы или при передаче отчетов аналитикам для последующего анализа в Microsoft Excel.
В этой статье мы подробно рассмотрим, как решить эту задачу максимально эффективно. Мы проанализируем популярные решения с форумов, выявим их скрытые недостатки и опасности, а затем разработаем надежный, быстрый и универсальный алгоритм выгрузки, соответствующий международным стандартам обмена данными.
На форумах часто предлагают простые алгоритмы, которые на первый взгляд кажутся идеальными из-за лаконичности кода. Рассмотрим типичный пример такого решения, основанный на последовательном обходе строк таблицы и записи данных через объект ТекстовыйДокумент:
ТекстДок = Новый ТекстовыйДокумент;
Для каждого Стр Из ТаблицаЗагрузки Цикл
СтрокаВыгрузки = "";
Для а = 0 по ТаблицаЗагрузки.Колонки.Количество() - 1 Цикл
СтрокаВыгрузки = СтрокаВыгрузки + ";" + Строка(Стр[а]);
КонецЦикла;
// Удаляем первый лишний разделитель
СтрокаВыгрузки = Сред(СтрокаВыгрузки, 2);
ТекстДок.ДобавитьСтроку(СтрокаВыгрузки);
КонецЦикла;
ТекстДок.Записать(ПутьКФайлу);
Преимущества этого подхода: предельная простота понимания и реализации. Код пишется буквально за пару минут и отлично работает на небольших тестовых наборах данных (до 100-200 строк).
Недостатки и опасности:
+) в цикле заставляет платформу 1С каждый раз выделять в оперативной памяти место под новую строку и копировать туда старые данные. Если в вашей таблице десятки тысяч строк, этот процесс займет очень много времени.ТекстовыйДокумент полностью загружает весь формируемый файл в оперативную память сервера. При попытке выгрузить миллион строк вы можете легко столкнуться с перерасходом ОЗУ и аварийным завершением работы рабочего процесса 1С.Другой альтернативный вариант, часто обсуждаемый разработчиками, — использование объекта ПостроительОтчета или вывод данных в ТабличныйДокумент с последующим сохранением в текстовый формат. Этот способ хорош тем, что автоматически использует стандартные представления 1С для всех типов данных. Однако он является крайне ресурсоемким. Визуальный вывод данных в ячейки табличного документа занимает в десятки раз больше времени и памяти процессора, чем прямая работа с текстовыми потоками.
Чтобы написать по-настоящему профессиональный инструмент выгрузки, нам нужно решить три ключевые технологические задачи:
ЗаписьТекста. Он работает в режиме буферизированного потока: данные небольшими порциями сбрасываются на жесткий диск, благодаря чему расход оперативной памяти стремится к нулю независимо от размера выгружаемой таблицы.СтрСоединить(). Он выполняет склеивание элементов массива строк на уровне платформенного C++ кода, работая в разы быстрее обычного сложения в цикле 1С.Также важно уделить пристальное внимание форматированию базовых типов данных 1С:
1 500.50 с неразрывным пробелом в качестве разделителя групп). Внешние программы не смогут распознать такое значение как число. Нам необходимо принудительно отключать группировку, используя Формат(Значение, "ЧГ=0").ГГГГ-ММ-ДД ЧЧ:ММ:СС).1 или 0, либо англоязычные аналоги true / false.Рассмотрим подробный программный код универсальной процедуры выгрузки, спроектированной с учетом всех вышеописанных требований производительности и надежности.
// Процедура осуществляет выгрузку произвольной ТаблицыЗначений в текстовый файл с разделителями.
// Параметры:
// ТаблицаДанных - ТаблицаЗначений, которую необходимо выгрузить.
// ПутьКФайлу - Строка, полный путь к создаваемому файлу.
// Разделитель - Строка, символ-разделитель колонок (по умолчанию ";").
//
Процедура ВыгрузитьТЗвCSV(ТаблицаДанных, ПутьКФайлу, Разделитель = ";") Экспорт
// Инициализируем поток записи. Используем кодировку UTF-8 как наиболее универсальную.
ЗаписьФайла = Новый ЗаписьТекста(ПутьКФайлу, КодировкаТекста.UTF8);
Колонки = ТаблицаДанных.Колонки;
КоличествоКолонок = Колонки.Количество();
// Шаг 1. Формируем и записываем строку заголовков (шапку файла)
МассивЗаголовков = Новый Массив;
Для Каждого Колонка Из Колонки Цикл
// Если у колонки заполнен заголовок, берем его, иначе используем системное имя
ИмяШапки = ?(ЗначениеЗаполнено(Колонка.Заголовок), Колонка.Заголовок, Колонка.Имя);
МассивЗаголовков.Добавить(ЭкранироватьЗначениеДляCSV(ИмяШапки, Разделитель));
КонецЦикла;
// Записываем собранную шапку в файл
ЗаписьФайла.ЗаписатьСтроку(СтрСоединить(МассивЗаголовков, Разделитель));
// Шаг 2. Обходим строки таблицы и выгружаем данные
Для Каждого СтрокаТЗ Из ТаблицаДанных Цикл
МассивСтроки = Новый Массив;
Для Каждого Колонка Из Колонки Цикл
ТекущееЗначение = СтрокаТЗ[Колонка.Имя];
СтроковоеПредставление = "";
// Выполняем строгое приведение типов данных к текстовому стандарту
ТипТекущегоЗначения = ТипЗнч(ТекущееЗначение);
Если ТипТекущегоЗначения = Тип("Число") Тогда
// Исключаем разделители групп (пробелы) и фиксируем системную точку для дроби
СтроковоеПредставление = Формат(ТекущееЗначение, "ЧГ=0; ЧДЦ=2; ЧРД=.");
ИначеЕсли ТипТекущегоЗначения = Тип("Дата") Тогда
// Форматируем дату в международный стандарт ISO 8601
Если ТекущееЗначение = '00010101000000' Тогда
СтроковоеПредставление = ""; // Пустую дату выгружаем как пустую строку
Иначе
СтроковоеПредставление = Формат(ТекущееЗначение, "ДФ=yyyy-MM-dd HH:mm:ss");
EndЕсли;
ИначеЕсли ТипТекущегоЗначения = Тип("Булево") Тогда
// Преобразуем булево в понятный для баз данных числовой флаг
СтроковоеПредставление = ?(ТекущееЗначение, "1", "0");
ИначеЕсли ТекущееЗначение = Неопределено Или ТекущееЗначение = Null Тогда
СтроковоеПредставление = "";
Иначе
// Для ссылок и прочих типов получаем их стандартное строковое представление
СтроковоеПредставление = Строка(ТекущееЗначение);
КонецЕсли;
// Экранируем полученную строку перед добавлением в массив
МассивСтроки.Добавить(ЭкранироватьЗначениеДляCSV(СтроковоеПредставление, Разделитель));
КонецЦикла;
// Записываем готовую строку данных в файл
ЗаписьФайла.ЗаписатьСтроку(СтрСоединить(МассивСтроки, Разделитель));
КонецЦикла;
// Обязательно закрываем файл для сохранения изменений и освобождения ресурсов ОС
ЗаписьФайла.Закрыть();
КонецПроцедуры
// Вспомогательная функция для обработки спецсимволов по стандарту RFC 4180
Функция ЭкранироватьЗначениеДляCSV(ВходноеЗначение, Разделитель)
Результат = Строка(ВходноеЗначение);
// 1. Если внутри текста есть кавычки, мы должны удвоить их
Результат = СтрЗаменить(Результат, """", """""");
// 2. Если в тексте присутствуют разделитель, кавычки или перенос строки,
// то все поле целиком необходимо обернуть в двойные кавычки.
Если Найти(Результат, Разделитель) > 0
Или Найти(Результат, """") > 0
Или Найти(Результат, Символы.ПС) > 0
Или Найти(Результат, Символы.ВК) > 0 Тогда
Результат = """" + Результат + """";
КонецЕсли;
Возврат Результат;
КонецФункции
Давайте подробно разберем, как именно функционируют наиболее важные участки представленного программного кода.
Объект ЗаписьТекста создается с указанием конкретной кодировки текста. Мы выбрали КодировкаТекста.UTF8, поскольку она поддерживается практически всеми современными информационными системами без риска получить нечитаемые символы («кракозябры»). Если принимающая сторона работает исключительно на старых версиях Windows, кодировку можно переключить на ANSI (или КодировкаТекста.Системная).
Сборка полей осуществляется через динамический массив Новый Массив. Мы последовательно обходим ячейки текущей строки, форматируем их значения, пропускаем через функцию экранирования спецсимволов и складываем в этот массив. После завершения обхода строки мы мгновенно преобразуем массив в готовую текстовую строку методом СтрСоединить(МассивСтроки, Разделитель). Это исключает лишнее выделение оперативной памяти под промежуточные операции конкатенации строк.
Функция ЭкранироватьЗначениеДляCSV берет на себя всю рутину по подготовке сложного текста к выгрузке. Проанализируем пример: если в ячейке хранится строка ООО "Рога и Копыта"; г. Москва, функция сначала удвоит внутренние кавычки, превратив текст в ООО ""Рога и Копыта""; г. Москва. Затем, обнаружив в тексте символ разделителя «точка с запятой», функция обернет всю фразу в дополнительные внешние кавычки. На выходе мы получим идеально валидную для стандарта CSV строку: "ООО ""Рога и Копыта""; г. Москва". Любой корректный парсер CSV (включая Microsoft Excel) без труда распознает эту конструкцию и поместит исходный текст в одну-единственную ячейку без повреждения структуры таблицы.
Для наглядности сопоставим рассмотренные подходы к выгрузке таблиц значений в текстовый формат по важнейшим критериям эффективности:
| Критерий сравнения | Простой цикл (ТекстовыйДокумент) | Построитель Отчета (ТабДок) | Эталонный метод (ЗаписьТекста + СтрСоединить) |
|---|---|---|---|
| Скорость работы | Низкая (замедляется пропорционально объему) | Очень низкая (высокие затраты на рендеринг) | Максимально высокая (потоковая запись на C++) |
| Потребление ОЗУ | Высокое (весь файл удерживается в памяти) | Критически высокое (создается графическая сетка) | Минимальное (данные пишутся порциями напрямую на диск) |
| Устойчивость к спецсимволам | Отсутствует (структура файла ломается) | Частичная (зависит от настроек конвертера) | Полная (соответствие международному RFC 4180) |
| Форматирование типов данных | Только стандартное (непригодное для интеграций) | Хорошее стандартное представление 1С | Гибко настраиваемое ручное форматирование |
Как видно из таблицы, использование связки ЗаписьТекста и СтрСоединить() превосходит альтернативные варианты по всем ключевым параметрам. Мы настоятельно рекомендуем использовать именно этот подход при написании любых обработок обмена данными в 1С:Предприятии 8.