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

Быстрый список листов в Delphi через HotXLS GetSheetNames

Иногда единственный вопрос, на который нужен ответ приёмной процедуре, чисто структурный: есть ли в этой книге лист с именем «Mapping» и сколько в ней вообще вкладок. Отвечать на него вызовом Open — дорогой способ. Полное открытие разворачивает таблицу общих строк, декодирует каждую запись стиля и обходит ячейки каждого листа, потому что ему неоткуда узнать, что вам нужно было только оглавление. На большом файле это сотни мегабайт выделений и несколько секунд процессорного времени ради списка, занимающего пару килобайт. HotXLS, нативная библиотека электронных таблиц для Delphi от losLab, отдаёт этот список отдельно: GetSheetNames возвращает имена листов в порядке книги, не материализуя ни одной ячейки

Почему каталог дёшево читать

Оба табличных формата держат своё оглавление ближе к началу, и именно это делает вызов перечисления быстрым, а не хитрым. Пакет OOXML хранит каталог листов в xl/workbook.xml — этой части всё равно, десять в книге строк или десять миллионов. BIFF8 .xls складывает записи BoundSheet в начале потока глобальных данных книги, до любых данных ячеек. Так что работа, которой избегает вызов перечисления, — не погрешность округления по сравнению с полным открытием. Это большая часть файла. Чтение каталога стоит одних и тех же нескольких килобайт независимо от числа строк, тогда как полное открытие растёт вместе с данными, и на многомегабайтной книге этот разрыв доходит до нескольких порядков и по прочитанным байтам, и по выделенной памяти

HotXLS GetSheetNames в Delphi читает только каталог листов файла XLSX или XLS, тогда как полное открытие обходит каждую ячейку
Каталог лежит в workbook.xml или в записях BoundSheet, поэтому перечисление стоит несколько килобайт, а полное открытие растёт вместе с данными

Именно эта плоская стоимость и есть свойство, вокруг которого стоит проектировать. Приёмный шлюз, построенный на GetSheetNames, ведёт себя одинаково и на файле в 200 строк, и на файле в 200 МБ, поэтому самый медленный файл в пачке больше не задаёт темп решению о том, стоит ли файл обработки вообще

Один вызов для .xls, .xlsx и форматов шаблонов

На фасаде XLS метод TXLSWorkbook.GetSheetNames читает не только .xls. Он принимает и построенные на zip .xlsx, .xlsm, .xltx и .xltm, доставая из архива только workbook.xml. Для настоящего .xls он сканирует записи BoundSheet и останавливается на первой записи EOF подпотока глобальных данных, поэтому большой двоичный файл всё равно обходится лишь начальными килобайтами. У фасада XLSX есть гарантия, которая для долгоживущего сервисного кода значит больше, чем кажется на первый взгляд: TXLSXWorkbook.GetSheetNames оставляет экземпляр книги и не сброшенным, и не заполненным, поэтому экземпляр, уже держащий открытый документ, может прощупывать другие файлы, не тревожа тот, что у него в руках. GetODSSheetNames применяет тот же подход к пакетам OpenDocument, и у каждого из этих вызовов есть перегрузка с потоком, позволяющая исследовать загрузку, которая никогда не попадает на диск

var
  Book: TXLSXWorkbook;
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Book := TXLSXWorkbook.Create;
  try
    if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
      raise Exception.Create('unreadable workbook package');
    if Names.IndexOf('Mapping') < 0 then
      raise Exception.Create('required Mapping sheet is missing');
    for I := 0 to Names.Count - 1 do
      Writeln(Format('sheet %d: %s', [I, Names[I]]));
  finally
    Book.Free;
    Names.Free;
  end;
end;

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

Файлы .xlsm с макросами и форматы шаблонов перечисляются ровно как обычный .xlsx, поскольку каталог лежит в том же workbook.xml независимо от того, едет ли в пакете vbaProject.bin. Поэтому приёмный конвейер может перечислить листы книги с макросами ради маршрутизации, ни разу не тронув полезную нагрузку макроса и не сделав ничего, что бы его запустило, а решение о политике по макросам оставить той стадии, которая действительно открывает файл

Как читать возвращаемое значение и не обмануть себя

Соглашения о возвращаемых значениях в HotXLS не единообразны. Одни вызовы возвращают 1 при успехе, другие возвращают количество, поэтому для функций перечисления единственная надёжная проверка — считать отказом любое значение, равное нулю или меньше, при очищенном списке строк. Не поддавайтесь соблазну прочитать пустой список как «книгу без листов». И ECMA-376, и спецификация BIFF8 требуют в корректной книге хотя бы один лист, поэтому ноль имён всегда означает неудачное чтение, а не законно пустой файл

Неудачное перечисление и само по себе сигнал, который стоит сохранить. Файл .xlsx, проваливший этот вызов, — что-то из короткого списка: усечённый, вовсе не пакет OOXML (сюда постоянно попадают экспорты CSV с чужой этикеткой из других систем) или зашифрованный контейнер. Различить их — задача следующей проверки. Запись первых байтов отклонённого файла рядом с ошибкой обычно превращает переписку с поддержкой в одно сообщение

Обнаружение зашифрованных контейнеров до маршрутизации

Зашифрованный .xlsx — не zip. Это составной файл OLE, оборачивающий потоки EncryptionInfo и EncryptedPackage, поэтому GetSheetNames не может заглянуть внутрь и возвращает отказ, как для любого другого нечитаемого файла. CanReadEncrypted проверяет именно форму такого контейнера, что позволяет приёмнику осознанно направить зашифрованный файл по нужному маршруту, а не глотать общую ошибку чтения откуда-то из глубины рабочего процесса:

Схема сортировки входящих файлов в Delphi: HotXLS CanReadEncrypted и GetSheetNames направляют загрузки в ветки нужен пароль, нечитаемый или обычный
CanReadEncrypted выполняется первым, потому что зашифрованный файл OOXML — это контейнер OLE, внутрь которого вызовы перечисления заглянуть не могут
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // Зашифрованный OOXML - контейнер OLE, а не zip: проверяем первым,
    // потому что вызовы перечисления не могут заглянуть внутрь него.
    if Book.CanReadEncrypted(FileName) then
      Exit(irNeedsPassword);
    if SameText(ExtractFileExt(FileName), '.ods') then
    begin
      if Book.GetODSSheetNames(FileName, Names) <= 0 then
        Exit(irUnreadable);
    end
    else if Book.GetSheetNames(FileName, Names) <= 0 then
      Exit(irUnreadable);
    Result := irNormal;
  finally
    Book.Free;
  end;
end;

Шифрование — то место, где HotXLS намеренно асимметричен, и маршрутизация обязана это учитывать. Устаревшее шифрование .xls (RC4, RC4 CryptoAPI, XOR) читается: TXLSWorkbook.Open(FileName, Password) расшифровывает файл сохранённым паролем, и такие файлы могут остаться на автоматическом пути. С зашифрованными пакетами OOXML всё наоборот. HotXLS умеет записать такой пакет через SaveAsEncrypted, но прочитать его обратно не может. OpenEncrypted возбуждает EXlsxEncryptionNotImplemented, получив зашифрованный пакет, и поэтому честная схема приёмки отправляет зашифрованный .xlsx человеку с Excel, а .xls с паролем оставляет в коде

В пакетной обработке этот классификатор оправдывает себя тем, что проходит по всему входящему каталогу до того, как рабочие процессы начнут настоящую работу: каждая проба стоит примерно одного открытия файла и нескольких килобайт чтения. Вынос его вперёд меняет тот режим отказа, который на самом деле волнует эксплуатацию. Вместо задания, умирающего в три часа ночи на файле 412 из 600, вы получаете 412 файлов в очереди и 5 отклонённых на приёмке с указанной причиной у каждого. Те же вызовы библиотеки, куда лучшая эксплуатационная картина

Вопросы, на которые вызов перечисления ответить не может

Имена и порядок — это всё, что вы получаете. Вызовы перечисления ничего не говорят о видимости, поэтому скрытые и очень скрытые листы приходят в списке наравне с прочими. Они не сообщают размеров используемого диапазона, числа ячеек и свойств документа. Часть docProps/core.xml тоже невелика, но пробы только по свойствам сегодня нет, поэтому метаданные автора и заголовка по-прежнему стоят полного Open. Чистый способ с этим жить — пустить дешёвые факты на маршрутизацию каждого файла, а дорогие приберечь для файлов, переживших маршрутизацию. Для файлов, которые всё-таки идут в глубокое чтение, сканирование большого .xls только на чтение заметно ускоряется с _DisableGraphics := True, пропускающим разбор OfficeArt. Только никогда не сохраняйте из такого экземпляра: пропущенный слой рисунков исчез из модели, и сохранение выбросило бы его из файла

Файлы, прошедшие сортировку, обычно идут в более глубокий анализ. Верстак аудита и конвертации книг разбирает счётчики по листам, которые стоит собирать, когда полное открытие уже оправдано, а руководство по производительности на больших книгах рассказывает, как сохранить это полное открытие быстрым

HotXLS — нативная библиотека электронных таблиц на Object Pascal для Delphi и C++Builder; вся поверхность API, включая показанные здесь вызовы инспекции, описана на странице продукта HotXLS Delphi Component