Иногда единственный вопрос, на который нужен ответ приёмной процедуре, чисто структурный: есть ли в этой книге лист с именем «Mapping» и сколько в ней вообще вкладок. Отвечать на него вызовом Open — дорогой способ. Полное открытие разворачивает таблицу общих строк, декодирует каждую запись стиля и обходит ячейки каждого листа, потому что ему неоткуда узнать, что вам нужно было только оглавление. На большом файле это сотни мегабайт выделений и несколько секунд процессорного времени ради списка, занимающего пару килобайт. HotXLS, нативная библиотека электронных таблиц для Delphi от losLab, отдаёт этот список отдельно: GetSheetNames возвращает имена листов в порядке книги, не материализуя ни одной ячейки
Почему каталог дёшево читать
Оба табличных формата держат своё оглавление ближе к началу, и именно это делает вызов перечисления быстрым, а не хитрым. Пакет OOXML хранит каталог листов в xl/workbook.xml — этой части всё равно, десять в книге строк или десять миллионов. BIFF8 .xls складывает записи 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 проверяет именно форму такого контейнера, что позволяет приёмнику осознанно направить зашифрованный файл по нужному маршруту, а не глотать общую ошибку чтения откуда-то из глубины рабочего процесса:
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