На вопрос «определяет ли эта книга имя 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 несут то, что объявил файл. Просто у каждого листа ноль ячеек
Именно на этих данных работает аудит. Какие книги прячут лист? В каких до сих пор определён 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 рапортует выборочное чтение как неподдерживаемое
Почему 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 — единственное, разбирающее ячейки, и разбирает оно лишь листы, которые выбрал проход один
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