Основная идея: взять заранее подготовленный шаблон xlsx файла с пустым листом, запихнуть в нужное место несжатые (zip-формат это позволяет) XML-данные листа и пересчитать некоторые байты в заранее известных местах. Такой способ нетребователен к CPU, но получаемые файлы значительно больше по размеру, чем обычные.
Для начала рассмотрим структуру zip-архива
Пример архива из двух файлов. Источник CodeProject
Как видно каждый файл кодируется четырьмя блоками, два из которых опциональны. Каждый блок имеет специальную сигнатуру (последовательность из 4-х байт), которая в текстовом редакторе выглядит как PK.., что позволяет легко определять начало этих блоков. Local Header помимо сигнатуры содержит поля: размер сжатых данных файла, размер несжатых данных, контрольную сумму CRC32 для данных и другие. Central Header содержит те же данные, что и File Header, на который ссылается, а так же указание места, где File Header находится (число байт от начала файла, так называемый offset) и некоторые другие. End of Central Dir (EOCD) содержит число файлов в архиве и место первого Central Header. Формат zip предполагает обработку с конца файла, т.е. сначала читается EOCD секция, потом Central Header нужного файла, по нему находится File Header и потом выполняется переход к сжатым данным. Более подробное описание структур и полей можно увидеть здесь.
Стоит отметить, что каждый файл, входящий в архив, может иметь свой алгоритм и степень сжатия, задаваемые в Local Header. Архив одновременно может содержать как сжатые файлы, так и нет, чем собственно и можно воспользоваться, задав файлу, содержащему данные листа, нулевое сжатие, что позволит добавлять в секцию FileData XML-текст как он есть. После вставки новых данных первого файла необходимо пересчитать не только размер и контрольную сумму в Local Header #1 и обновить Central Header #1, но и увеличить смещение в Central Header #2 (и других тоже, если они есть) на длину добавленных данных. Это не сложно, но можно избежать, если менять не первый, а последний (в данном случае второй) файл. xlsx-файл с одним листом содержит 9 файлов. Данные листа хранятся в
xl\worksheets\sheet1.xml
. Чтобы сделать этот файл последним в архиве, надо всего лишь удалить его из архива, а потом заново добавить с нулевым сжатием.Примерный вид
sheet1.xml
(добавлены переносы строк и отступы для читаемости)<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheetViews>
<sheetView tabSelected="1" workbookViewId="0"/>
</sheetViews>
<sheetFormatPr defaultRowHeight="12.75"/>
<sheetData></sheetData>
<phoneticPr fontId="0" type="noConversion"/>
<pageMargins left="0.75" right="0.75" top="1" bottom="1" header="0.5" footer="0.5"/>
<headerFooter alignWithMargins="0"/>
</worksheet>
Hex-данные исправленного архива в Hex-редакторе HxD.
Обратите внимание, что используется обратный порядок байт, так сигнатура
0x04034b50
записана как 50 4b 03 04
, а длина файла sheet1.xml
— 0x0000020C
= 0x20C = 524 (байт) как 0С 02 00 00
.struct LocalFileHeader
{
uint32_t signature; // Обязательная сигнатура, равна 0x04034b50
uint16_t versionToExtract; // Минимальная версия для распаковки
uint16_t generalPurposeBitFlag; // Битовый флаг
uint16_t compressionMethod; // Метод сжатия (0 - нет, 8 - deflate)
uint16_t modificationTime; // Время модификации файла
uint16_t modificationDate; // Дата модификации файла
uint32_t crc32; // Контрольная сумма
uint32_t compressedSize; // Сжатый размер
uint32_t uncompressedSize; // Несжатый размер
...
Открыв архив, в hex-редакторе визуально легко найти данные
sheet1.xml
. Чуть выше находится сигнатура PK — это и будет началом File Header. Встав на нее справа (красная полоска), получим выделение слева (толстый красный квадрат) и позицию 0x17E3
в строке состояния — это положение от начала файла заголовка File Header для файла sheet1.xml
.Учитывая, что тип uint32_t — это 4 байта, а uint16_t — два, получаем следующую картинку, где
голубой прямоугольник — сигнатура, оранжевый — метод сжатия, два серых — дата и время изменения файла и синий — это контрольная сумма, за которой идут два зеленых, содержащих сжатый и несжатый размеры.
Чтобы внести изменения после вставки данных листа на позицию 3, отмеченную стрелочкой, необходимо из hex-редактора выписать в скольких байтах от начала файла находятся следующие поля (ниже приводятся получившиеся значения в моем файле
template.xlsx
):- CRC =
0x17F1
. Добавив +4 получится смещение для сжатого размера, и еще +4 несжатого - Начало данных файла (стрелка 2) =
0x1819
- Место куда будут дописаны данные (стрелка 3) =
0x1969
- Конец данных файла =
0x1A24
- CRC в Central Header для
sheet1.xml
=0x1C69
- Отступ в EOCD =
0x1CAF
и старое значение в нем0x1A25
, которое нужно будет увеличить на длину добавленных данных
После этого можно приступать к генерации файла:
- В массив байт читаются данные шаблона
- Вычисляется «накапливаемый» CRC32, сначала по данным шаблона от начала данных (2) до тега
sheetData
(3), потом по вставляемым данным, а потом отsheetData
до конца файла - Обновляются биты CRC и длины в структурах FileHeader и Central Header в массиве байт
- Формируется результирующий массив, как данные файла из шаблона до тега
sheetData
+ вставляемые данные + данные шаблона послеsheetData
, который и будет итоговым результатом.
С самой генерацией XML данных листа проблем возникнуть не должно, однако стоит отметить, что числа хранятся как XML-узлы:
<c><v>100</v></c>
Строки же, как:
<c t="s"><v>2</v></c>
где
2
указывает на второй узел в файле xl\sharedStrings.xml
. Таким образом Excel экономит место, храня одинаковые строки как одно значение. Чтобы не менять в архиве еще и
sharedString
, строки можно писать сразу в sheet1.xml
, применив атрибут inlineStr
:<c t="inlineStr"><is><t>I'll be back</t></is></c>
Не стоит забывать про маскирование спец-символов XML —
< > & ' "
символов. В итоге должно получаться что то вида<row>
<c><v>100</v></c>
<c t="inlineStr"><is><t>AAA</t></is></c>
</row>
<row>
<c><v>200</v></c>
<c t="inlineStr"><is><t>BBB</t></is></c>
</row>
Сам Excel заполняет больше XML-атрибутов, например номера строк и используемые диапазоны, но они опциональны.
Демо-программа доступна здесь, а файл шаблона здесь
Комментарии (8)
mal_ls
17.08.2021 14:18Формат zip допускает сценарий, когда в zip файл дописывают данные. По указанной вами ссылке это упоминается
Иногда бывает невозможно вычислить данные на момент записи
LocalFileHeader
, тогда вcrc32
,compressedSize
иuncompressedSize
записываются нули, третий бит вgeneralPurposeBitFlag
ставится в единицу, а послеLocalFileHeader
добавляется структура типаDataDescriptor
.но подробно не описано.
В случае с формированием xlsx, docx и pptx, наверное его лучше использовать? Создаёшь заготовку xlsx; удаляешь из него xl\worksheets\sheet1.xml; подготавливаешь архив; а потом при отдаче в поток дописываешь xl\worksheets\sheet1.xml с нужными данными.little-brother Автор
17.08.2021 14:39Если есть задача формировать документы для отдачи, напр. по http, то да, можно использовать и способ дописывания размера в DataDescriptor. С другой стороны приведенный способ, пойдет только для небольших файлов, т.к. не используется sharedStrings и компрессию, а для таких файлов объем занимаемой памяти десяток килобайт (из них шаблон - 7Кб), так что не проблема сформировать целый документ прямо в памяти.
staticmain
*Новый* файл Excel. Старый формат — это бинарная каша.
Sergani
Не бинарная каша, а COM Storage. Объект со своей внутренней файловой системой.
staticmain
www.loc.gov/preservation/digital/formats/fdd/fdd000510.shtml
>Microsoft Office Excel 97-2003 Binary File Format (.xls)
Binary — бинарный.
docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06
107 МБ — каша.
То что оно абстрактно представлено как файловая система с кучей самозависимостей и противоречий не отменяет того факта что это бинарная каша.
pewpew
Все файлы — бинарная каша. То, что старый формат был плохо описан и даже теперь документация по нему — тёмный лес, вовсе не отменяет того что это вполне структурированная бинарная каша. И потом, старый формат уже архаизм. Кто им пользуется?
А по существу статьи — ну да, можно свой zlib написать, хоть без сжатия, хоть с ним. И XML — это вообще текст, если на то пошло. Не совсем понятно, в чём достижение. И зачем. Формат ZIP прекрасно описан, как и алгоритмы сжатия. Это можно самому написать на любом ЯП.
staticmain
Есть текстовые человекочитаемые форматы, типа xml, есть бинарные форматы (типа mxf), а есть бинарная каша, типа xls.
Вы явно никак не взаимодействовали с государственными структурами.
kest007
А как же очень старые версии эксель? Там, вроде, OLE1. А OLE1 !≈ COM :)