Техническая статья

Патчинг одного листа в большом XLSX из Delphi

HotXLS умеет переписывать один лист внутри существующего пакета XLSX без разбора или повторного сжатия остальной части файла. TXLSDirectWriter.BeginPatch открывает исходный пакет, копирует каждую запись, кроме целевого листа, с сохранением сжатых байтов дословно и позволяет заново авторить этот единственный лист через обычные вызовы AddSheet, AddRow и Write*. Диаграммы, кеши сводных таблиц, темы, стили и общие строки вообще никогда не распаковываются

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

Почему копирование сжатых байтов — самая интересная часть?

Запись zip, скопированная на уровне сжатых данных, стоит одного копирования потока. Та же запись, прошедшая через обычный путь записи, стоит распаковки на входе и сжатия на выходе, а сжатие — дорогая половина. На книге с большим кешем сводной таблицы и несколькими десятками встроенных изображений эта разница — разница между патчем, завершающимся за время записи нового листа, и патчем, тратящим большую часть времени на повторное сжатие байтов, которые он ни разу не рассматривал

HotXLS использует для этого CopyCompressedFrom, который записывает сжатые байты исходной записи прямо в целевой архив. Когда запись невозможно скопировать таким образом — из-за иного метода сжатия или слабого шифрования — писатель откатывается к копированию через распаковку, а не завершается ошибкой. Служебные маркерные записи каталога пропускаются, поскольку писатель формирует собственные

Замена на месте или запись в новый файл

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

var
  W: TXLSDirectWriter;
begin
  W := TXLSDirectWriter.Create;
  try
    W.BeginPatch('monthly-dashboard.xlsx', 'Data');   // на месте
    W.AddSheet('Data');
    W.AddRow(1);
    W.WriteString(1, 'Region');
    W.WriteString(2, 'Revenue');
    W.AddRow(2);
    W.WriteString(1, 'North');
    W.WriteNumber(2, 184320.55);
    W.AddRow(3);
    W.WriteFormula(1, '=SUM(B2:B2)');
    W.Close;
  finally
    W.Free;
  end;
end;

Вариант со вставкой принимает исходный и целевой путь плюс InsertSheet:

  // Источник остаётся нетронутым; цель получает дополнительный лист Extra
  W.BeginPatch('template.xlsx', 'output.xlsx', 'Extra', True);
  W.AddSheet('Extra');
  W.AddRow(1);
  W.WriteString(1, 'appended by the nightly job');
  W.Close;

Вставка — та часть, что требует настоящей хирургии учёта. Писатель разбирает реестр листов в xl/workbook.xml и карту связей, привязывающую каждый лист к его части, затем выбирает следующий свободный номер части, идентификатор листа и идентификатор связи. Типы связей следуют соглашениям исходного пакета, поэтому патчинг строгой книги ISO 29500 выводит строгие типы связей, а патчинг переходной книги — переходные

Что патч сознательно отбрасывает и ограничивает

Цепочка вычислений отбрасывается в обоих режимах. В режиме замены её записи описывают ячейки листа, которого больше не существует в этой форме; в режиме вставки сдвиг индекса листа делает её попросту недействительной. Excel пересобирает цепочку при следующем пересчёте, так что отбросить её — правильно, а не с потерями. Часть исключается из копии, а её запись связи и переопределение типа содержимого удаляются хирургически

Две семантики авторинга внутри патча меняются, и обе следуют из одного принципа: патч не должен нарушать части, которые он не переписывал. Строки записываются встроенно в лист, а не добавляются в таблицу общих строк, поскольку исходная таблица переносится нетронутой. А StyleIndex ссылается на записи в cellXfs исходного пакета, а не на таблицу стилей, которую строит писатель. Это означает, что вы можете ссылаться на форматы, уже определённые исходной книгой, что обычно именно то, чего хочет обновление данных, но также означает, что нужно знать, какой индекс несёт какой формат

// Внутри патча StyleIndex индексирует cellXfs ИСХОДНОГО пакета.
// Дате нужен явный индекс, отображающийся на формат даты там:
W.WriteDateTime(3, EncodeDate(2026, 8, 22), DateStyleIndexFromTemplate);

// Перегрузка WriteDateTime без стиля отклоняется в режиме патча,
// поскольку она предполагает наличие собственной таблицы стилей
// писателя, которую патч никогда не создаёт

Шесть точек входа авторинга закрыты: добавление таблиц, диаграмм, изображений, комментариев, именованных диапазонов и стилей ячеек — всё это выбрасывает исключение в режиме патча, с дополнительной страховкой при закрытии, которая завершается ошибкой, если любой из их счётчиков ненулевой. Каждая из этих функций потребовала бы редактирования частей, которые патч копирует дословно, а наполовину отредактированный пакет хуже отклонённой операции. За одну операцию можно патчить ровно один лист

Когда патчить, а когда загружать

Патчинг — правильный инструмент, когда книга большая, изменение ограничено одним листом, а остальная часть файла должна сохраниться побитово. Это неправильный инструмент, когда изменение затрагивает несколько листов, требуется новое форматирование или новые объекты, или файл достаточно мал, чтобы обычная загрузка и сохранение не стоили ничего. Для массовой генерации с нуля потоковый путь, описанный в статье потоковый прямой писатель, остаётся лучшим вариантом, и он использует тот же API AddRow и Write*, так что переход между двумя способами механический

Манипуляции на уровне листа внутри загруженной книги, когда действительно нужна полная объектная модель, описаны в статье дублирование листов в пакетах XLSX. А если причина, по которой вы рассматриваете патч, — что обработка целой книги стала медленной, измерения и поведение памяти в статье производительность больших книг стоит прочитать перед выбором подхода

Проверка, что патч действительно сделал то, что нужно

Три проверки ловят почти все ошибки. Убедитесь, что части, которые должны были сохраниться, всё ещё в архиве, что xl/calcChain.xml отсутствует, и что повторное открытие файла через TXLSXWorkbook сообщает ожидаемое число листов — неизменное для замены и увеличенное на один для вставки. Чтение пропатченного листа и сравнение нескольких значений и формул замыкает цикл проверки

Одна деталь реализации из разработки этой функции заслуживает повторения, поскольку может укусить любого, кто пишет похожий код на уровне zip. Имена частей листов сопоставляются по префиксу, и ошибка на единицу в длине префикса означает, что предикат никогда не совпадёт, поэтому вновь записанная часть конфликтует с существующим именем, а читатели, берущие последнюю запись с данным именем, молча выбирают не тот лист. Если патч, кажется, поменял местами содержимое двух листов, смотрите на сопоставление имён раньше, чем на XML

Патчинг на месте, потоковая запись и полная объектная модель книги поставляются в одной библиотеке для Delphi и C++Builder; список возможностей — на странице компонента HotXLS для электронных таблиц Delphi