Цель
В рамках импортозамещения (не используя COM), сохранить текущую функциональность выгрузок в Excel:
- Преобразование чисел и дат к внутреннему формату Excel (фильтры по диапазонам чисел, дат)
- Добавление колонтитулов (Распечатано оттуда-то тогда-то)
- Преобразование ссылок из обычной строки в Гиперссылку
- Сохранение множества листов в одну книгу Excel
НЕМНОГО О РЕАЛИЗАЦИИ
Числа и даты
1. Числа
Исследуем, как сохраняется форматирование числа при использовании метода ТабличныйДокумент.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.XLSX)
Для этого сгенерируем небольшой файл со следующим содержимым:
Распакуем файл XLSX
Разберем вхождение
<c r="B4" s="5" t="n">
<v>42.012</v>
</c>
Каждая ячейка описывается тэгом "c", и содержит следующие атрибуты (см. стандарт ecma-376):
r (Reference) - содержит ссылку на местоположение ячейки (например: "B4", "A20" и т.п.)
s (Style Index) - содержит индекс стиля данной ячейки, s="5" указывает, что 5-е (отсчитываемое от нуля) вхождение тэга <xf> в <cellXfs> файла "\xl\styles.xml"
t (Cell Data Type) - содержит тип ячейки, в нашем случае "n"
Доступные типы ячеек представлены в таблице ниже:
Значение | Описание |
b (Boolean) | Ячейка, содержащая булево |
d (Date) | Ячейка, содержащая дату |
e (Error) | Ячейка, содержащая число |
inlineStr (Inline String) | Ячейка, содержащая строку, используется при сложном форматировании строки. В остальных случаях используются Общие строки |
n (Number) | Ячейка, содержащая число |
s (Shared String) | Ячейка, содержащая Общую строку (отсчитываемый от нуля индекс вхождения тэга <si> в <sst> в файле \xl\\xl\worksheets\sheet1.xml |
str (String) | Ячейка, содержащая формулу |
Разберем 5-е (отсчитываемое от нуля) вхождение тэга <xf> в <cellXfs>
<xf numFmtId="52" applyNumberFormat="true" applyAlignment="true">
<alignment horizontal="right"/>
</xf>
Нас интересует numFmtId, который указывает на числовой формат ячейки, описываемый в тэге <numFmts> файла "\xl\styles.xml"
<numFmt numFmtId="52" formatCode="0.000"/>
Таким образом, чтобы получить форматную строку ячейки, нужно:
1. Получить атрибут s ячейки <c> в файле листа (например, \xl\worksheets\sheet1.xml)
2. Получить соответствующее атрибуту s (отсчитываемое от нуля) вхождение тэга <xf> в <cellXfs> в файле "\xl\styles.xml" и получить атрибут numFmtId
3. Найти тэг <numFmt> в <numFmts> в том же файле "\xl\styles.xml" и получить атрибут formatCode
Выводы по числам и выявленные особенности:
1. При выгрузке табличного документа в Excel 1С сохраняет числа как числа, если в ячейке было установлен соответствующий ТипЗначения и Значение (см. "код генерации табличного документа с числами разрядностью от 0 до 19"). При выгрузке отчетов из СКД ТипЗначения и Значение для чисел устанавливаются автоматически.
2. Номера числовых форматов в тэге <numFmts> файла "\xl\styles.xml" начинаются с 50
3. Для чисел с разрядностью 0 используется numFmtId="1", для чисел с разрядностью 2 используется numFmtId="2", описание встроенных форматов не нашел, но номера сходятся с номером (отсчитываемым от нуля) перечисления в разделе "(все форматы)"
4. Для чисел, с разрядностью больше 13, при выгрузке создается "свой" формат для каждого числа, например, <numFmt numFmtId="63" formatCode="[=42.01234567890123]"42,01234567890123""/>
5. Формат чисел (formatCode) с заданной разрядностью равен "0."+ "0", повторенный [Разрядность] раз
2. Даты
сгенерируем небольшой файл, содержащий:
в колонке 1 - текстовое представление даты
в колонке 2 - значение "Дата" (Новый ОписаниеТипов("Дата",,, Новый КвалификаторыДаты(ЧастиДаты.Дата)))
в колонке 3 - значение "ДатаВремя" (Новый ОписаниеТипов("Дата",,, Новый КвалификаторыДаты(ЧастиДаты.ДатаВремя)))
Распакуем файл XLSX
И убедимся, что все ячейки в файле "\xl\worksheets\sheet1.xml" имеют одинаковый формат в атрибуте s
<c r="A2" s="1" t="s">
<c r="B2" s="1" t="s">
<c r="C2" s="1" t="s">
А так же в файле "\xl\styles.xml" присутствует только один стиль для ячеек
<xf applyAlignment="true">
<alignment horizontal="left"/>
</xf>
Сделаем вывод, что 1С не поддерживает сохранение дат во внутренний формат Excel.
Excel имеет собственный формат даты, дата представляется в виде числа, которое содержит количество суток, прошедших с 01 января 1900 года, увеличенных на единицу.
При этом Excel считает 1900 год високосным, т.е. в Excel существует дата 29 февраля 1900 года.
Вероятно, 1С не стало реализовывать преобразование даты во внутренний формат Excel из-за невозможности преобразования дат меньших, чем 01 января 1900 года. Хотя, будем надеяться, что в ближайших версиях платформы данная возможность появится, а даты ранее 01 января 1900 года будут выгружаться как строки.
С учетом вышеизложенного, можно реализовать функцию преобразования дат следующим образом:
Функция ДатаВФорматеЭксель(Дата)
Если Дата = '00010101' Тогда
Возврат 0;
КонецЕсли;
//Excel считает 1900 год високосным
Если Дата < '19000301' Тогда
Значение = 1;
Иначе
Значение = 2;
КонецЕсли;
Возврат Значение + (Дата - '19000101') / 86400
КонецФункции
Дополним Код генерации табличного документа с датами 2мя колонками (с "Дата" и "ДатаВремя" во внутреннем формате Excel).
И убедимся, что после установки формата, колонки 1 (дата текстом) и 5 (дата в формате dd.mm.yyyy hh:mm:ss) равны.
После преобразования дат достаточно поменять стили в файле "\xl\styles.xml"
Предлагается следующий подход:
1. Для ячеек содержащих дату и время устанавливать разрядность 9, для даты - 8.
2. После сохранения XLSX, распаковывать архив, заменять
"#,##0.000000000" на "dd.mm.yyyy hh:mm:ss", а
"#,##0.00000000" на "dd.mm.yyyy"
Для выгрузки дат из СКД предлагается вместо использования функции ДатаВФорматеЭксель создавать следующее выражение:
ВЫБОР
КОГДА Дата = ДАТАВРЕМЯ(1, 1, 1)
ТОГДА 1
КОГДА Дата < ДАТАВРЕМЯ(1900, 3, 1)
ТОГДА 1 + РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1900, 1, 1), Дата, "ДЕНЬ")
ИНАЧЕ 2 + РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1900, 1, 1), Дата, "ДЕНЬ")
КОНЕЦ + (ЧАС(Дата) * 60 * 60 + МИНУТА(Дата) * 60 + СЕКУНДА(Дата)) / 86400
Достоинства метода - высокая скорость:
1. Не используется ЧтениеXML, ЗаписьXML, ПостроительDOM
2. Вне зависимости от количества листов в книге, замена происходит только в одном месте (файл "\xl\styles.xml")
Недостатки:
1. Нет гарантии, что в следующих версиях платформы не изменится формат выгрузки в XLSX
2. Числа с разрядностью 8 и 9 нельзя использовать в выгрузках (во избежание пересечения с датами)
Гиперссылки
Для добавления ссылки, например, на сайт "ya.ru", в ячейке "A1" необходимо:
1. в разметку в файле Листа "\xl\worksheets\sheet1.xml" добавить разметку следующего содержания:
<hyperlinks>
<hyperlink ref="A1" r:id="rId4"/>
</hyperlinks>
Где
ref - содержит ссылку на местоположение ячейки (например: "B4", "A20" и т.п.)
r:id - содержит идентификатор связи в соответствующем файле связей "\xl\worksheets\_rels\sheet1.xml.rels"
2. В разметку в файле связей "\xl\worksheets\_rels\sheet1.xml.rels" добавить разметку следующего содержания:
<Relationships ...>
...
<Relationship Id="rId4" Target="http://ya.ru" TargetMode="External" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"/>
</Relationships>
Где
Id - содержит идентификатор связи
Target - href ссылки, например "http://ya.ru"
TargetMode и Type - для гиперссылок имеют значения "External" и "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" соответственно
Общий алгоритм работы:
1. Устанавливаем у ячеек в табличном документе стиль гиперссылки (синий шрифт и подчеркивание)
2. Устанавливаем в шапке (в обработке анализируется первая строка) табличного документа синий шрифт
3. Для всех колонок, у которых в шапке установлен синий шрифт, обходятся ячейки и генерируется текст для тэгов <hyperlinks> и <Relationships>. Если в ячейке задана расшифровка, то "href ссылки" получается из поля "Расшифровка", в противном случае, "href ссылки" и представление ссылки получаются из текста ячейки
4. Быстрая вставка сгенерированного текста тэгов <Relationships> в файлы связей \xl\worksheets\_rels\sheetN.xml.rels осуществляется в конец файлового потока, предварительно отступив закрывающий тэг </Relationships>
5. Остается проблема быстрой вставки сгенерированного текста тэгов <hyperlinks> в файл Листа "\xl\worksheets\sheetN.xml", он должен быть вставлен между тэгами <sheetData> и <pageMargins>, в противном случае книга не открывается
При выгрузке 100 000 строк в Excel и открытии файла через ПостроительDOM, сервер 1С съедает несколько ГБ оперативной памяти.
В обработке реализовано решение, основанное на фиксации содержимого файла, начиная с pageMargins. Перед сохранением табличного документа всем полям (сверху, снизу, слева, справа) устанавливается значение 0. Ожидается что в табличном документе не изменены масштаб и ориентация страниц, т.е. при выгрузке рассчитываем, что окончание XML каждого листа - константа. Запись осуществляется в конец файлового потока, предварительно отступив "окончание XML листа".
<pageMargins left="0" top="0" right="0" bottom="0" header="0" footer="0"/>
<pageSetup blackAndWhite="false" scale="100" pageOrder="overThenDown" orientation="portrait"/>
<headerFooter alignWithMargins="true" scaleWithDoc="true"/>
<drawing r:id="rId1"/>
<legacyDrawing r:id="rId2"/>
</worksheet>
При записи поля устанавливаются жестко (по 10 мм с каждого края).
Достоинства метода - высокая скорость:
1. Не используется ЧтениеXML, ЗаписьXML, ПостроительDOM
Недостатки:
1. Нет гарантии, что в следующих версиях платформы не изменится формат выгрузки в XLSX
2. Решение не подходит, если необходимо выгружать картинки (тэг drawing).
Колонтитулы
1С не сохраняет колонтитулы в Excel, несмотря на наличие соответствующих реквизитов у табличного документа.
Суть метода сводится к замене сгенерированного 1С тэга <headerFooter> в файле Листа "\xl\worksheets\sheetN.xml"
<headerFooter alignWithMargins="true" scaleWithDoc="true"/>
на
<headerFooter alignWithMargins="true" scaleWithDoc="true">
<oddHeader>&L[Верхний колонтитул]</oddHeader>
<oddFooter>&L[Нижний колонтитул]</oddFooter>
</headerFooter>
Сохранение множества листов
Данный функционал реализован в 1С через ПакетОтображаемыхДокументов.
Также функционал полезно использовать при необходимости отображения наименования листа в нижнем левом углу книги Excel (сохранение одного листа через ПакетОтображаемыхДокументов см. СохранитьТабличныйДокументВXLSXOpenXML в приведенных функциях ниже).
Функция СохранитьСписокТабличныхДокументовВXLSX(СписокЛистов)
Книга = Новый ПакетОтображаемыхДокументов;
Для Каждого Лист Из СписокЛистов Цикл
ТабличныйДокумент = Лист.Значение;
ЛистExcel = Книга.Состав.Добавить();
ЛистExcel.Данные = ПоместитьВоВременноеХранилище(ТабличныйДокумент);
ЛистExcel.Наименование = Лист.Представление;
КонецЦикла;
ПутьКФайлу = ПолучитьИмяВременногоФайла("XLSX");
Книга.Записать(ПутьКФайлу, ТипФайлаПакетаОтображаемыхДокументов.XLSX);
Возврат ПутьКФайлу;
КонецФункции
Функция СохранитьСписокТабличныхДокументовВXLSXOpenXML(ИмяФайла, СписокЛистов,
ВерхнийКолонтитул = Неопределено,
НижнийКолонтитул = Неопределено) Экспорт
НастроитьОформлениеСпискаДокументовДляВыгрузкиOpenXML(СписокЛистов);
ПутьКФайлу = СохранитьСписокТабличныхДокументовВXLSX(СписокЛистов);
Если ВерхнийКолонтитул = Неопределено Тогда
ВерхнийКолонтитул = "";
КонецЕсли;
Если НижнийКолонтитул = Неопределено Тогда
НижнийКолонтитул = НижнийКолонтитулПоУмолчанию(ИмяФайла);
КонецЕсли;
ПрименитьСтилиXLSXВФайлеOpenXML(ПутьКФайлу, СписокЛистов, ВерхнийКолонтитул, НижнийКолонтитул);
ДвоичныеДанные = Новый ДвоичныеДанные(ПутьКФайлу);
УдалитьФайлы(ПутьКФайлу);
Возврат ДвоичныеДанные;
КонецФункции
Функция СохранитьТабличныйДокументВXLSXOpenXML(ИмяФайла, ТабличныйДокумент, НаименованиеЛиста = "", ВерхнийКолонтитул = Неопределено, НижнийКолонтитул = Неопределено) Экспорт
СписокЛистов = Новый СписокЗначений;
СписокЛистов.Добавить(ТабличныйДокумент, ?(ЗначениеЗаполнено(НаименованиеЛиста),
НаименованиеЛиста,
НСтр("ru = 'Лист1'; en = 'Sheet1'")));
Возврат СохранитьСписокТабличныхДокументовВXLSXOpenXML(ИмяФайла, СписокЛистов, ВерхнийКолонтитул, НижнийКолонтитул);
КонецФункции
Реализация тестировалась на платформе 8.3.14.2032 (z), win64
Кому подойдет данная реализация
1. У вашего заказчика есть одно из вышеперечисленных требований (см. Цели)
2. Вы не можете использовать COM Объект Excel
3. Требуется высокая производительность и ваши выгрузки объемны (или использование ПостроительDOM вызывает проблемы)
4. Вы готовы к тому, что при обновлении платформы, функционал может перестанет работать, т.к. может измениться формат и последовательность тэгов XML, формируемых при выгрузке в Excel платформой 1С, и готовы инвестировать время в расследование причин
5. Вы не используете при выгрузке картинки, либо готовы самостоятельно реализовать альтернативный механизм вставки тэгов <hyperlinks> в файлы листов "\xl\worksheets\sheetN.xml"
Производительность в разрезе используемых функций в зависимости от количества строк, мс
Функция / Количество строк в таблице |
1000 строк |
10000 строк |
20000 строк |
50000 строк |
РаспаковатьXLSX | 250 | 531 | 1015 | 1766 |
ДобавитьГиперссылкиXLSXВПапкеOpenXML | 328 | 2938 | 6250 | 14813 |
ДобавитьКолонтитулыXLSXВПапкеOpenXML | 16 | 16 | 16 | 16 |
УстановитьСтилиДатXLSXВПапкеOpenXML | 16 | 16 | 31 | 16 |
ЗапаковатьXLSX | 234 | 484 | 1000 | 1344 |
Использование в СКД
1. Для полей ДатаИВремя создаем вычисляемые поля с типом Число(15,9) со следующем выражением:
ВЫБОР
КОГДА Дата = ДАТАВРЕМЯ(1, 1, 1)
ТОГДА 1
КОГДА Дата < ДАТАВРЕМЯ(1900, 3, 1)
ТОГДА 1 + РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1900, 1, 1), Дата, "ДЕНЬ")
ИНАЧЕ 2 + РАЗНОСТЬДАТ(ДАТАВРЕМЯ(1900, 1, 1), Дата, "ДЕНЬ")
КОНЕЦ + (ЧАС(Дата) * 60 * 60 + МИНУТА(Дата) * 60 + СЕКУНДА(Дата)) / 86400
для полей Дата устанавливаем тип - Число(15,8) и не добавляем дробную часть ((ЧАС(Дата) * 60 * 60 + МИНУТА(Дата) * 60 + СЕКУНДА(Дата)) / 86400)
2. Для ссылок - делаем макет поля (Параметр - текст ссылки, например "Товар 1", Параметр расшифровки - href ссылки, например, "http://myshop.ru/?id=1"), оформление - произвольное (например ЦветТекста = Новый Цвет(51, 102, 255), Шрифт - подчеркнутый). В условном оформлении поля со ссылкой добавляем ЦветТекста = Новый Цвет(51, 102, 255), Область применения = "В заголовке полей" (обязательно, т.к. по этому признаку определяется будет ли колонка отформатирована как гиперссылка)
Использование при формировании табличного документа "вручную" (например, при использовании макета)
1. Ячейки с датами обрабатываем следующим образом (текст функции ДатаВФорматеЭксель см. ниже)
//Для Дата
Область.СодержитЗначение = Истина;
Область.ТипЗначения = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15,8));
Область.Значение = Цел(ДатаВФорматеЭксель(Дата));
//Для ДатаИВремя
Область.СодержитЗначение = Истина;
Область.ТипЗначения = Новый ОписаниеТипов("Число", Новый КвалификаторыЧисла(15,9));
Область.Значение = ДатаВФорматеЭксель(Дата);
2. Ссылки оформляем аналогично СКД (заголовок колонки: обязательно ЦветТекста = Новый Цвет(51, 102, 255), в ячейках Параметр - текст ссылки, например, "Товар 1", Параметр расшифровки - href ссылки, например, "http://myshop.ru/?id=1")
Сохранение табличного документа в Excel
//Один лист
ДвоичныеДанные = СохранитьТабличныйДокументВXLSXOpenXML(
"Имя файла в нижний колонтитул",
ТабличныйДокумент,
"ПервыйЛист", //Необязательный параметр
"Верхний колонтитул", //Необязательный параметр
//Необязательный параметр НижнийКолонтитул, если Неопределено используется НижнийКолонтитулПоУмолчанию()
);
ДвоичныеДанные.Записать(ИмяФайлаXLSX); //При необходимости
//Несколько листов
СписокЛистов = Новый СписокЗначений;
СписокЛистов.Добавить(ТабличныйДокумент1, "ПервыйЛист");
СписокЛистов.Добавить(ТабличныйДокумент2, "ВторойЛист");
ДвоичныеДанные = СохранитьСписокТабличныхДокументовВXLSXOpenXML(
"Имя файла в нижний колонтитул",
СписокЛистов,
"Верхний колонтитул", //Необязательный параметр
//Необязательный параметр НижнийКолонтитул, если Неопределено используется НижнийКолонтитулПоУмолчанию()
);
ДвоичныеДанные.Записать(ИмяФайлаXLSX); //При необходимости
Функция НижнийКолонтитулПоУмолчанию(ИмяФайла)
Возврат СтрШаблон(НСтр(
"ru = '%1 дата выгрузки %2
|Распечатано из такой-то системы';
|en = '%1 created on %2
|Printed from such-and-such system)'"),
ИмяФайла,
Формат(ТекущаяДата(), НСтр("ru = 'ДФ=''dd.MM.yyyy hh:mm:ss'''; en = 'ДФ=''yyyy-dd-MM h:mm:ss tt'''")));
КонецФункции
Внедрение
1. Вынести область OpenXML в любой серверный общий модуль
2. Найти по тексту конфигурации вхождения ТипФайлаТабличногоДокумента.XLSX и, если производится сохранение табличного документа, реализовать вызов функции СохранитьТабличныйДокументВXLSXOpenXML