Что делает процедура выгрузки:
1. Клиентская процедура получает в качестве входных параметров текст запроса и структуру параметров, и имена колонок(необязательный)
2. Серверная процедура выполняет запрос и получая построчно результат запроса записывает его потоково в текстовый файл через объект типа ЗаписьТекста, не держа его в оперативной памяти
3. На сервере у сформированного текстового файла изменяется расширение на csv и он запаковывается в архив
4. Клиентская процедура получает файл распаковывает его и открывает файл через Excel.
Описание
Наверное, всех раздражает долгая выгрузка сформированных отчетов в Excel через «Сохранить как» для последующего анализа. А так же кучу недовольства вызывает когда оставляешь такую выгрузку на ночь и утром видишь сообщение «Недостаточно памяти на клиенте» или «Недостаточно памяти на сервере». Как по мне так самое узкое место во всей 1С. Я перепробовал много вариантов:
1. Сохранение в MXL и конвертация в Excel средствами программы «1С Работа с файлами»
2. Сохранение в HTML4 и открытие этого файла через Excel
3. Сохранение через COM-объект Excel напрямую из запроса в файл на клиенте
Предлагаемый мною способ по скорости первышает самый быстрый из этих трех (HTML4) примерно в полтора — два раза, а стандартное сохранение из 1С в Excel в 8 раз(таблица 65 колонок 240 000 строк, заполнение 90%, за 1 час 11 минут, раньше она выгружалась около 6 часов). Сразу оговорюсь что оценки времени могут сильно отличатся, поскольку это зависит от производительности и загруженности сервер и клиента, а так же сетевого соединения.
После таких опытов стало понятно что самое узкое место это передача несжатой таблице с сервера на клиент и отрисовка ее на клиенте. Поэтому возникла идея сделать выгрузку сразу на сервере в файл, сжать его и отправить на клиент.
Но общеизвестно что сформированный 1с 8 табличный документ занимает очень много оперативной памяти. Например таблица с 65 колонками и 240 000 строк, у меня съела 900 Mб памяти. В таком виде конечно никакой сервер не справится. Поэтому возникла идея, записывать файл построчно, без накопления в оперативной памяти. И для этого как нельзя лучше подходит формат файла с разделителями CSV, который можно открыть через Excel и поработав с ним , сохранить в xlsx. CSV можно формировать потоково, так как это обычный текстовый файл. Есть ряд граблей при работе с этим форматом, такие как удаление символов переноса строки и символа «;», а так же то что Excel удаляет в строках левые нули, превращая в число. Все эти проблемы я в приведенных ниже процедурах решил.
Решение выкладываю в виде шести процедур в двух общих модулях
Первый модуль, компилируемый только на сервере
Функция ВыгрузитьВФайлЧерезСервер(ЗапросТекст, МассивНазванийПараметров, МассивЗначенийПараметров, МассивИменПолейЗапроса, МассивИменПолейОтчета = Неопределено) Экспорт
Запрос = Новый Запрос;
Запрос.Текст = ЗапросТекст;
ПорядковыйНомерПараметра = 0;
Пока ПорядковыйНомерПараметра <= МассивНазванийПараметров.Количество() - 1 Цикл
НазваниеПараметра = МассивНазванийПараметров[ПорядковыйНомерПараметра];
ЗначениеПараметра = МассивЗначенийПараметров[ПорядковыйНомерПараметра];
Запрос.УстановитьПараметр(НазваниеПараметра, ЗначениеПараметра);
ПорядковыйНомерПараметра = ПорядковыйНомерПараметра + 1;
КонецЦикла;
ВремяПередЗапросом = ТекущаяДата();
Результат = Запрос.Выполнить();
ВремяПослеЗапроса = ТекущаяДата();
Выборка = Результат.Выбрать();
КаталогВР = КаталогВременныхФайлов();
ИмяФайла = ПолучитьИмяВременногоФайла("csv");
ЗТ = Новый ЗаписьТекста(ИмяФайла);
//Потоковое чтение
Сообщить(Строка(ТекущаяДата()) + " - Окончание выполнения запроса к серверу(Запрос выполнен за " +Строка(ВремяПослеЗапроса - ВремяПередЗапросом) + " секунд)");
СтрокаФайла = "№;";
Если МассивИменПолейОтчета <> Неопределено Тогда
ПерваяСтрокаФайла = МассивИменПолейОтчета;
Иначе
ПерваяСтрокаФайла = МассивИменПолейЗапроса;
КонецЕсли;
Для Каждого ИмяПоля Из ПерваяСтрокаФайла Цикл
СтрокаФайла = СтрокаФайла + ОбработатьТекст(ИмяПоля) + ";";
КонецЦикла;
ЗТ.ЗаписатьСтроку(СтрокаФайла);
Индекс = 0;
Пока Выборка.Следующий() Цикл
СтрокаФайла = Строка(Индекс) + ";";
Для Каждого ИмяПоля Из МассивИменПолейЗапроса Цикл
ТестЯчейки = ОбработатьТекст(Выборка[ИмяПоля]);
СтрокаФайла = СтрокаФайла + ТестЯчейки + ";";
КонецЦикла;
ЗТ.ЗаписатьСтроку(СтрокаФайла);
Если Индекс/10000 = Цел(Индекс/10000) и Индекс <> 0 Тогда
Сообщить("Выгружено " + Индекс + " строк");
КонецЕсли;
Индекс = Индекс + 1;
КонецЦикла;
ЗТ.Закрыть();
Сообщить("Выгружено всего " + Индекс + " строк");
ИмяАрхива = ПолучитьИмяВременногоФайла("zip");
ЗаписьZIP = Новый ЗаписьZipФайла(ИмяАрхива);
// Добавим необходимые файлы в архив
ЗаписьZIP.Добавить(ИмяФайла);
// Запишем архив на диск
ЗаписьZIP.Записать();
УдалитьФайлы(ИмяФайла);
ФайлАрхива = Новый ДвоичныеДанные(ИмяАрхива);
ХЗ = Новый ХранилищеЗначения(ФайлАрхива);
//Сообщить(Строка(ТекущаяДата()) + " - Окончание формирования файла на сервере");
УдалитьФайлы(ИмяАрхива);
Возврат ХЗ;
КонецФункции
Функция ОбработатьТекст (Знач ТестЯчейки)
ТестЯчейки = СтрЗаменить(ТестЯчейки, ";", " ");
ТестЯчейки = СтрЗаменить(ТестЯчейки, Символы.ПС, " ");
ТестЯчейки = СтрЗаменить(ТестЯчейки, """", " ");
ТестЯчейки = СтрЗаменить(ТестЯчейки, "'", " ");
ТестЯчейки = СтрЗаменить(ТестЯчейки, Символ(160), " ");
Возврат ТестЯчейки;
КонецФункции
Второй модуль, компилируемый исключительно на клиенте
Процедура ВыгрузитьИзЗапросаВCSVНаКлиенте(ЗапросТекст, ЗапросПараметры, МассивИменПолейЗапроса = Неопределено, МассивИменПолейОтчета = Неопределено) Экспорт
ДиалогФыбораФайла = Новый ДиалогВыбораФайла(РежимДиалогаВыбораФайла.Сохранение);
ДиалогФыбораФайла.Фильтр = "CSV документ(*.csv)|*.csv";
ДиалогФыбораФайла.Заголовок = "Выберите файл для выгрузки данных";
ДиалогФыбораФайла.ПредварительныйПросмотр = Ложь;
ДиалогФыбораФайла.ПолноеИмяФайла = Лев(ТекущаяДата(), 10);
Если МассивИменПолейЗапроса = Неопределено Тогда
МассивИменПолейЗапроса = ПолучитьМассивИменПолейПоТекстуЗапроса(ЗапросТекст);
КонецЕсли;
Путь = "";
Если ДиалогФыбораФайла.Выбрать() Тогда
//Выгружаем на сервере в файл
//Так как сейчас передаем данные с клиента на сервер, есть ограничения, запрос нельзя, список значения нельзя, массив(а так же массив массивов) и простые типы можно
СтруктураПараметров = ПреобразоватьПараметрыЗапросаВСтруктуруДвухМассивов(ЗапросПараметры);
ХЗДвДанные = БольшиеОтчетыСервер.ВыгрузитьВФайлЧерезСервер(ЗапросТекст, СтруктураПараметров.НазваниеПараметров, СтруктураПараметров.ЗначениеПараметров, МассивИменПолейЗапроса, МассивИменПолейОтчета);
Путь = ДиалогФыбораФайла.Каталог;
ПутьФайлаАрхива = Путь + "ЖРДС_Выгрузка.zip";
ХЗДвДанные.Получить().Записать(ПутьФайлаАрхива);
ЧтениеZipФайла = Новый ЧтениеZipФайла(ПутьФайлаАрхива);
ЧтениеZipФайла.ИзвлечьВсе(Путь);
ПереместитьФайл(Путь + ЧтениеZipФайла.Элементы[0].Имя,ДиалогФыбораФайла.ПолноеИмяФайла);
УдалитьФайлы(ПутьФайлаАрхива);
КонецЕсли;
КонецПроцедуры
Функция ПреобразоватьПараметрыЗапросаВСтруктуруДвухМассивов(ПараметрыЗапроса)
СтруктураМассивов = Новый Структура;
НазваниеПараметров = Новый Массив;
ЗначениеПараметров = Новый Массив;
Для Каждого ПараметрЗапроса Из ПараметрыЗапроса Цикл
НазваниеПараметров.Добавить(ПараметрЗапроса.Ключ);
Если ТипЗнч(ПараметрЗапроса.Значение) = Тип("СписокЗначений") Тогда
ЗначениеПараметров.Добавить(ПреобразоватьСЗВМассив(ПараметрЗапроса.Значение));
Иначе
ЗначениеПараметров.Добавить(ПараметрЗапроса.Значение);
КонецЕсли;
КонецЦикла;
СтруктураМассивов.Вставить("НазваниеПараметров", НазваниеПараметров);
СтруктураМассивов.Вставить("ЗначениеПараметров", ЗначениеПараметров);
Возврат СтруктураМассивов;
КонецФункции
Функция ПреобразоватьСЗВМассив(СписокЗначений)
Массив = Новый Массив;
Для Каждого ЭлементСЗ Из СписокЗначений Цикл
Массив.Добавить(ЭлементСЗ.Значение);
КонецЦикла;
Возврат Массив;
КонецФункции
Функция ПолучитьМассивИменПолейПоТекстуЗапроса(ТекстЗапроса) Экспорт
П_О = Новый ПостроительОтчета;
П_О.Текст = ТекстЗапроса;
П_О.ЗаполнитьНастройки();
МассивИменПолейЗапроса = Новый Массив;
Для Каждого ИмяПоля Из П_О.ВыбранныеПоля Цикл
МассивИменПолейЗапроса.Добавить(ИмяПоля.Имя);
КонецЦикла;
Возврат МассивИменПолейЗапроса;
КонецФункции
Как то пробовал удалить временный файл, вылетает с ошибкой:
У вас не так? Какая версия платформы?
Хорошая идея, осталось вставить строку запуска Excel на открытие файла и расстановку разделителей. Конечные пользователи не всегда любят возиться с форматированием.
Кстати, можешь попробовать выгружать в DBF, там, конечно, есть свои ограничения, но форматировать колонки не надо, плюс платформа напрямую с этим типом работает, Да и сам Excel открывает DBF без лишних вопросов. Кстати, можешь и оценить, что быстрее и удобнее.
Есть сложный отчет на СКД, возвращающий на экран не плоскую, а с группировками, таблицу. Колонок много, строк тоже (несколько сотен тысяч). Ваш код как-то может помочь? То есть я его встраиваю в отчет, вешаю вызов на кнопку «Сохранить в csv»?
(3) Dach, с группировками вряд ли выгрузишь, из csv импортируется как «текст с разделителями», если группировки не нужны — то тогда хоть как выгружай, хотя я сторонник таблиц, а не текстов. Попробуй как в примере справки в DBF сливать игнорируя группировки.
Показать
Пара замечаний (надеюсь, конструктивных):
1 Вместо передачи двух массивов (например, МассивНазванийПараметров, МассивЗначенийПараметров) напрашивается передача Структуры.
2. Не понял зачем нужно получать МассивИменПолейЗапроса на клиенте (да ещё через Построитель). Можно передать МассивИменПолейЗапроса как есть на сервер, а там после выполнения запроса выполнить что-то вроде:
Маловато вы способов перепробовали. Есть ещё сохранение сразу в xml понятного экселю вида, и есть работа с COMSafeArray (там тоже теряется форматирование, но скорость замечательно высокая). Ваш способ — ну, так можно, но это не супер-пупер.
про ADO это по моему в 6м комменте было
А если отчет на столько большой, что компьютер его формирует 3 часа, то сколько человек его будет читать?
Пользователь не в состоянии поглотить столько информации. Может следует уменьшить детализацию?
Проблему нужно решать с «психологической» точки зрения, а не программной или аппаратной.
(2) eskor, спасибо за предложения, обязательно вставлю ЗапуститьПриложение(). Насчет форматирования не совсем вас понял, пользователи просто открывают Экселем, и работают а сохраняют уже в xlsx. А насчет выгрузки в DBF идея интересная, только возможно ли потоковая запись в этот формат. Каким бы вы способом предложили мне это сделать?
(3) Dach, к сожалению нет, эти процедуры выгружают только плоскую таблицу напрямую из запроса, к СКД так просто не прикрутишь
(5) q_i, спасибо, очень конструктивные замечания
(8) Serj1C, дело в том что пользователи используют эти огромные выборки в экселе для того что бы анализировать их средствами экселя в определенных разрезах, сворачивают их, делают сводные данный, используют фильтры. Или например для сравнения больших массивов данных из разных баз посредством функции ВПР(). Применений куча. Вы видимо мало работаете с 1с
(6) Yashazz, согласен, маловато, все что нашел в интернете. Предложенная вами идея выгрузки в xml файл мне очень понравилась, его действительно можно так же потоково сформировать и решить проблему форматирования. Жалко только что этот формат не поддерживает разворачиваемые группировки, которые создает 1с. Если бы поддерживал я бы обязательно на него переделал.
Про Com-safeArray немного не понял, не вижу возможности через него потоково формировать файл, или вы имеете ввиду сформировать весь эксель и потом разом записать?
(9) запись в dbf не сильно отличается от записи в текстовый файл, на скорость я никогда не жаловался. В далекие времена, когда перенос данных через xml был в зачаточном состоянии, только так данные и гонял. Главное удобство было в том, что данные как раз в Excel и проверялись на корректность. Вроде как заголовки колонок должны быть на латинице, хотя давно я с dbf не заморачивался.
самый простой и быстрый обмен — через Буфер обмена
(15) Serg O., Не совсем понял о чем вы. Здесь вроде как речь идет о выгрузке в файл