Technical Article

Бързо извличане на имената на листове в Delphi с HotXLS GetSheetNames

Понякога единственият въпрос, на който входящата рутина трябва да отговори, е структурен: има ли тази работна книга лист, наречен "Mapping", или колко раздели (tabs) съдържа тя. Отговорът на това чрез извикване на Open е скъпият начин да го направите. Пълното отваряне разширява таблицата за споделени низове (shared string table), декодира всеки запис за стил и обхожда клетките на всеки работен лист, тъй като няма как да знае, че сте искали само съдържанието. При голям файл това означава стотици мегабайти заделена памет и няколко секунди процесорно време, изразходвани за четене на списък, който заема няколко килобайта. HotXLS, оригиналната Delphi библиотека за електронни таблици на losLab, ви дава този списък самостоятелно: GetSheetNames връща имената на работните листове в реда на работната книга, без да визуализира нито една клетка.

Защо каталогът е евтин за четене

И двата формата за електронни таблици поставят своето съдържание близо до началото, което прави извикването за изброяване бързо, а не сложно. Пакетът OOXML съхранява каталога на листовете в xl/workbook.xml - част, която остава малка независимо дали работната книга съдържа десет реда или десет милиона. Двоичният .xls във формат BIFF8 съхранява своите BoundSheet записи в началото на глобалния поток на работната книга, преди каквито и да било данни за клетки. Така че работата, която извикването за списване избягва, не е грешка от закръгляне спрямо пълното отваряне. Това е по-голямата част от файла. Четенето на каталога струва същите няколко килобайта независимо от броя на редовете, докато пълното отваряне се мащабира с данните, а при работна книга от няколко мегабайта тази разлика достига няколко порядъка както при засегнатите байтове, така е и при заделената памет.

Тази фиксирана цена е свойството, около което си струва да се проектира. Входящ филтър, изграден върху GetSheetNames, се държи по един и същ начин при файл с 200 реда и такъв с размер 200 MB, така че най-бавният файл в пакета вече не определя темпото при решаването дали даден файл изобщо си струва да бъде обработен.

Едно извикване за .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, при който извикването се проваля, е едно от няколко специфични неща: отрязан (truncated), изобщо не е OOXML пакет (погрешно етикетирани CSV експорти от други системи се появяват тук постоянно) или е криптиран контейнер. Различаването им е задача на следващата проверка. Записването на първите байтове на отхвърления файл в лога заедно с грешката обикновено превръща темата за поддръжка в едно-единствено съобщение.

Откриване на криптирани контейнери преди маршрутизиране

Криптираният .xlsx не е zip. Той е OLE съставен файл (OLE compound file), обвиващ потоците 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
    // Encrypted OOXML is an OLE container, not a zip: check first,
    // because the listing calls cannot look inside it.
    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 в кода.

При пакетна работа този класификатор намира мястото си, като преглежда цяла входяця директория, преди да започне реалната обработка, тъй като всяка проверка струва приблизително едно отваряне на файл и няколко килобайта четене. Предварителното му пускане променя начина на отказ, който наистина интересува поддръжката. Вместо задача в 3 часа сутринта да умре на файл 412 от 600, получавате 412 файла на опашка и 5 отхвърлени на входа с прикачена причина за всеки. Същите извиквания на библиотеката, но много по-добра оперативна история.

Въпросите, на които извикването за изброяване не може да отговори

Имената и редът са всичко, което получавате. Извикванията за изброяване не казват нищо за видимостта, така че скритите (hidden) и много скритите (very-hidden) листове се появяват в списъка като всеки друг. Те не съобщават размери на използвания диапазон, брой клетки или свойства на документа. Частта docProps/core.xml също е малка, но днес няма сонда само за свойства, така че метаданните за автор и заглавие все още струват пълно Open. Чистият начин да се живее с това е да се оставят евтините факти да маршрутизират всеки файл и да се запазят скъпите за файловете, които преминават успешно маршрутизирането. За файловете, които продължават към цялостно четене, сканирането само за четене на голям .xls работи забележимо по-бързо с _DisableGraphics := True, което пропуска парсването на OfficeArt. Само никога не записвайте от тази инстанция: прескоченият графичен слой е изчезнал от модела и записването би го премахнало от файла.

Файловете, които преминават триажа, обикновено отиват към по-дълбок анализ. Работен плот за одит и преобразуване на работни книги обхваща броячите за всеки лист, които си струва да се събират след обосновка на пълно отваряне, а ръководството за производителност на големи работни книги обхваща това как това пълно отваряне да остане бързо.

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