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

Загрузка XLSX по метаданным и с выбором листов в HotXLS

На вопрос «определяет ли эта книга имя TaxRate?» отвечает одна маленькая часть пакета XLSX. Имена листов, их состояния hidden и very hidden и все определённые имена уровня книги лежат в xl/workbook.xml (ECMA-376 Part 1, §18.2) — обычно несколько килобайт. Ячейки живут в другом месте: по одной части xl/worksheets/sheetN.xml на лист плюс таблица общих строк, и именно там почти все байты. Загрузчик, настаивающий на построении полной книги прежде чем вы сможете о чём-то спросить, распакует и разберёт всё это — для каждого файла в каталоге — чтобы ответить на вопрос, который уже решили первые несколько килобайт

Разделение, которое имеет значение, — между чтением формы книги и чтением её веса. Загрузчик, умеющий остановиться после формы, загрузить вес для выбранного подмножества листов и дотянуть ещё один лист позже, превращает «открыть книгу» из шага «всё или ничего» в то, что вы ограничиваете на каждый вызов. С собой он приносит две ловушки: одна про базы индексов, другая — о том, как долго исходный файл обязан лежать на месте

Что читает Open только по метаданным и где останавливается

С TXLSXWorkbook.MetadataOnly равным True до Open, HotXLS разбирает часть книги, стили, тему, определённые имена, имена листов и состояния видимости, подключения, внешние ссылки и свойства документа из docProps/core.xml и docProps/app.xml. Затем он пропускает cell-XML каждого листа и спутниковые части каждого листа: комментарии, рисунки и таблицы. Таблица общих строк пропускается тоже — и это важнее, чем звучит: в насыщенных текстом книгах та одна часть может соперничать с листами по размеру

Возвращённая книга структурно полна. Sheets.Count верен, у каждого листа настоящее имя и Visibility, DefinedNames заполнен, ExternalLinks перечисляет все связанные цели, а Title, Author и LastModifiedBy несут то, что объявил файл. Просто у каждого листа ноль ячеек

HotXLS MetadataOnly читает свойства книги, имена листов, стили и ссылки в архиве XLSX в Delphi, затем останавливается до всякого XML ячеек листа
MetadataOnly читает каждую структурную часть архива и останавливается перед XML ячеек листов. Книга Delphi возвращается с именами листов, видимостью, именованными диапазонами и стилями — и нулём ячеек

Именно на этих данных работает аудит. Какие книги прячут лист? В каких до сих пор определён TaxRate — и на что? Какие ссылаются на файловую шару, выведенную из эксплуатации в прошлом году? Сканер ниже отвечает на все три вопроса для одного файла. Заметьте два соглашения об индексации бок о бок: Sheets[] нумеруется с единицы, как движок XLS, а DefinedNames и ExternalLinks — обычные списки с нуля

procedure ScanWorkbook(const FileName: WideString; Report: TStrings);
var
  Workbook: TXLSXWorkbook;
  Name: TXLSXDefinedName;
  I: Integer;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Workbook.MetadataOnly := True;           // читается при Open, поэтому выставляйте заранее
    if Workbook.Open(FileName) <> 1 then
    begin
      Report.Add(FileName + ': open failed');
      Exit;
    end;
    for I := 1 to Workbook.Sheets.Count do   // Sheets[] отсчитывается от 1
      case Workbook.Sheets[I].Visibility of
        xlsxSheetHidden:
          Report.Add(FileName + ': hidden sheet ' + Workbook.Sheets[I].Name);
        xlsxSheetVeryHidden:
          Report.Add(FileName + ': very hidden sheet ' + Workbook.Sheets[I].Name);
      end;
    Name := Workbook.DefinedNames.FindByName('TaxRate');
    if Name <> nil then
      Report.Add(FileName + ': TaxRate = ' + Name.Formula);
    for I := 0 to Workbook.ExternalLinks.Count - 1 do   // список с нуля
      Report.Add(FileName + ': links to ' + Workbook.ExternalLinks[I].Target);
  finally
    Workbook.Free;
  end;
end;

Сканер читает Visibility, а не булево Visible — нарочно. Visible равен False для обоих скрытых состояний, а very hidden лист — тот, который аудит мечет метку охотнее всего, ведь диалог Unhide в Excel его не перечисляет, и вернуть его может только VBA или прямая правка файла

Загрузка двух листов из сорока через SelectedSheets

SelectedSheets — TStringList, принадлежащий книге. Когда он держит хотя бы одно имя, Open разбирает cell-XML только для названных листов; каждый прочий лист создаётся с корректным именем и видимостью и без ячеек. Пустой список — по умолчанию — грузит все листы. Имена ищутся собственным IndexOf списка, поэтому с настройками по умолчанию совпадение игнорирует регистр — это сходится с правилом Excel, по которому два имени листов в одной книге не могут различаться только регистром

Workbook := TXLSXWorkbook.Create;
Workbook.SelectedSheets.Add('Summary');
Workbook.SelectedSheets.Add('Q3 Detail');
if Workbook.Open('consolidated-2026.xlsx') = 1 then
  ShowSheet(Workbook.Sheets[1]);   // ShowSheet — ваш собственный код UI

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

Избирательная загрузка проходит и через фазу параллельного разбора при включённом ParallelParse: невыбранные листы пропускаются внутри заданий воркеров, поэтому потоки тратят время на листы, о которых вы просили. Те же два свойства действуют, когда источник — бинарная книга .xlsb, с одним правилом области: .xlsb, несущий VBA-проект, обязан открываться без этих двух опций; при любой из выставленных Open рапортует выборочное чтение как неподдерживаемое

HotXLS SelectedSheets разбирает ячейки двух названных листов из сорока в Delphi, а LoadSheet материализует пропущенный лист из неизменённого исходного файла
SelectedSheets разбирает XML ячеек только для поименованных листов, оставляя остальные пустыми заглушками, хранящими имя и видимость. LoadSheet материализует заглушку позже, пока исходный файл всё ещё лежит на диске и не менялся

Почему LoadSheet(1) грузит не тот лист?

Потому что LoadSheet принимает позицию с нуля, а Sheets[] нумеруется с единицы. LoadSheet(1) материализует второй лист книги, и поскольку загрузка листа, у которого уже есть ячейки, — no-op, возвращающий True, перепутанный индекс может выглядеть успехом, пока лист, по которому кликнул пользователь, остаётся пустым. Конверсии место ровно в одном месте вашего кода

Вторая ловушка — время жизни файла. LoadSheet не держит архив открытым между вызовами. Он заново открывает исходный файл по имени, разбирает часть книги, стили и общие строки, затем разбирает запрошенный лист. Прежде чем довериться тому файлу, HotXLS сверяет его размер и время последней записи со значениями, снятыми при Open; если тем временем другой процесс пересохранил книгу, LoadSheet возвращает False вместо подмешивания ячеек другой версии в модель, которую вы уже держите

procedure TReportForm.ShowSheetNo(SheetNo: Integer);  // SheetNo считается с единицы, как Sheets[]
var
  Sheet: TXLSXWorksheet;
begin
  Sheet := FWorkbook.Sheets[SheetNo];
  if Sheet.Cells.Count = 0 then
    if not FWorkbook.LoadSheet(SheetNo - 1) then      // LoadSheet хочет позицию с нуля
    begin
      StatusBar.SimpleText := 'The workbook changed on disk; reopen it';
      Exit;
    end;
  Grid.ShowSheet(Sheet);                              // ваш собственный код грида
end;

Из этого дизайна следует несколько последствий. Книга, открытая из TStream, не имеет исходного файла, куда можно вернуться, поэтому LoadSheet возвращает False для .xlsx, открытого из потока; то же верно для зашифрованной книги, которая расшифровывается в память перед разбором. Бинарный .xlsb, избирательно открытый из потока, — исключение: HotXLS держит временную копию потока для отложенных листов. И по-настоящему пустой лист будет перечитываться при каждом вызове, ведь ноль ячеек — ещё и то, как выглядит пропущенный лист

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

Почему обход туда-обратно меняет части, которых вы не касались?

Сохранение, перегенерирующее всё, переписывает байты, которые никогда не менялись. Распакуйте тему, постройте объектную модель, сериализуйте её, сожмите снова — и результат отличается от оригинала, хотя никто тему не правил. Для VBA-проектов и кешей сводных таблиц цена выше шумного диффа: VBA-проект — бинарный составной файл с собственными внутренними потоками, а кеш сводной хранит снимок исходных данных, чьи поля определения сводных таблиц адресуют по позиции, поэтому самое безопасное с любым из них, пока его никто не менял, — не трогать его байты

PreserveRawParts, по умолчанию True, заставляет SaveAs копировать сжатые байты неизменённых частей прямо из исходного архива. Это покрывает тему, VBA-проект, подключения, диаграммы, встроенные медиа, настройки принтера, сводные таблицы и определения и записи кешей сводных — каждую лишь пока модель говорит, что она не модифицировалась. Те части никогда не распаковываются и не перепаковываются на пути через. Выставление свойства в False принуждает полную пересериализацию каждой части — тот переключатель, до которого стоит дотянуться, когда хотите, чтобы вывод отражал только то, что содержит объектная модель

Путь сырого копирования использует ту же проверку файла, что LoadSheet: источник должен всё ещё существовать с размером и отметкой времени, снятыми при Open. Когда это не так, SaveAs сериализует те части как обычно, и файл всё равно полный. Два случая всегда идут путём сериализации: книга, чьим источником был бинарный .xlsb, конвертируемый в XML при чтении, и сохранение с выставленным StrictOOXML, где меняются пространства имён частей

Сохранение частично загруженной книги требует решения. Для источника .xlsb SaveAs отказывается, пока хоть один лист отложен, и записывает «Load every deferred binary worksheet before saving» в Diagnostics. Для .xlsx обращайтесь с избирательно загруженной книгой как с read-view: у листа-заглушки нет ячеек, которые писатель мог бы выдать, поэтому либо вызовите LoadSheet для каждой заглушки сперва, либо переоткройте без выборки, прежде чем править и сохранять

Конвейер обнаружения, намеренно открывающий дважды

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

Двухпроходный конвейер обнаружения HotXLS в Delphi: проход только по метаданным решает, какие книги и листы важны, затем целевой проход разбирает лишь выбранные листы
Первый проход сканирует каталог только по метаданным и решает, какие файлы и листы важны; второй проход переоткрывает совпадения с выборкой листов. Только второе открытие вообще разбирает XML ячеек
procedure ProcessInvoiceFile(const FileName: WideString);
var
  Probe, Workbook: TXLSXWorkbook;
  Wanted: TStringList;
  I: Integer;
begin
  Wanted := TStringList.Create;
  try
    // Проход 1: обнаружение, без ячеек
    Probe := TXLSXWorkbook.Create;
    try
      Probe.MetadataOnly := True;
      if Probe.Open(FileName) <> 1 then
        Exit;
      for I := 1 to Probe.Sheets.Count do
        if Pos('Invoice', Probe.Sheets[I].Name) > 0 then
          Wanted.Add(Probe.Sheets[I].Name);
    finally
      Probe.Free;
    end;
    if Wanted.Count = 0 then
      Exit;                               // в этом файле нет ничего, что стоило бы разбора

    // Проход 2: ячейки только для выбранных листов
    Workbook := TXLSXWorkbook.Create;
    try
      Workbook.SelectedSheets.Assign(Wanted);
      if Workbook.Open(FileName) = 1 then
        PostInvoices(Workbook);           // ваш собственный код обработки
    finally
      Workbook.Free;
    end;
  finally
    Wanted.Free;
  end;
end;

Сброс MetadataOnly после Open не превращает пробник в полную книгу. Флаг читается при открытии; смена его после влияет лишь на следующий Open того же объекта. Пробник всё ещё может отращивать по листу за раз через LoadSheet, и для одного-двух листов это разумно, но конвейеру лучше служит чистое переоткрытие — оно к тому же приносит комментарии и таблицы каждого листа

Как это соотносится с другими рычагами больших файлов

Избирательная загрузка режет то, что вы разбираете. Для стоимости листа после его загрузки она не делает ничего, и не поможет, когда заданию нужны все ячейки всех листов. Тот случай — про хранение, о чём статья о блочном хранении ячеек по строкам и диапазонных стилях, и про пропускную способность разбора — параллельный разбор XLSX и распределитель памяти. Когда задача — чистое чтение без всякого редактирования, потоковый прямой читатель минует объектную модель книги целиком и лучше любой стратегии загрузки, такую модель строящей

Всё это работает внутри вашего собственного исполняемого файла Delphi или C++Builder, без установки Excel на сервере, который ведёт сканирование. Поддерживаемые форматы, редакции и лицензирование перечислены на странице компонента электронных таблиц HotXLS для Delphi