Technisch artikel

HotXLS: sheet listing and lightweight workbook inspection

Soms is de enige vraag die een intakeroutine moet beantwoorden structureel: heeft deze werkmap een blad genaamd "Mapping", of hoeveel tabbladen draagt hij. Dat beantwoorden door Open aan te roepen is de dure manier om het te doen. Een volledige open blaast de gedeelde-stringtabel op, decodeert elk stijlrecord, en doorloopt de cellen van elk werkblad, want het heeft geen manier om te weten dat je alleen de inhoudsopgave wilde. Op een groot bestand is dat honderden megabytes aan allocaties en meerdere seconden CPU-tijd besteed aan het lezen van een lijst die een paar kilobytes beslaat. HotXLS, de native Delphi-spreadsheetbibliotheek van losLab, geeft je die lijst op zichzelf: GetSheetNames geeft de werkbladnamen terug, in werkmapvolgorde, zonder ook maar één cel te materialiseren

Waarom de catalogus goedkoop is om te lezen

Beide spreadsheetformaten zetten hun inhoudsopgave dicht bij het begin, en dat is wat een lijstaanroep snel maakt in plaats van slim. Een OOXML-pakket houdt de bladcatalogus in xl/workbook.xml, een deel dat klein blijft ongeacht of de werkmap tien rijen of tien miljoen bevat. Een BIFF8-.xls slaat zijn BoundSheet-records op aan het begin van de workbook-globalsstream, vóór alle celdata. Het werk dat een lijstaanroep vermijdt is dus geen afrondingsfout tegenover een volledige open. Het is het grootste deel van het bestand. Het lezen van de catalogus kost dezelfde handvol kilobytes ongeacht het rijaantal, terwijl een volledige open meeschaalt met de data, en op een werkmap van meerdere megabytes loopt dat gat op tot meerdere ordes van grootte, zowel in aangeraakte bytes als in toegewezen geheugen

HotXLS GetSheetNames in Delphi dat alleen de bladcatalogus van een XLSX- of XLS-bestand leest, terwijl een volledige open elke cel doorloopt
De catalogus zit in workbook.xml of de BoundSheet-records, dus opsommen kost een paar kilobytes terwijl een volledige openactie meeschaalt met de data

Die platte kost is de eigenschap die het waard is om omheen te ontwerpen. Een intakepoort gebouwd op GetSheetNames gedraagt zich hetzelfde op een bestand van 200 rijen en een van 200 MB, dus het traagste bestand in een batch bepaalt niet langer het tempo voor de beslissing of een bestand het überhaupt waard is om te verwerken

Eén aanroep over .xls, .xlsx en de sjabloonformaten heen

Op de XLS-facade leest TXLSWorkbook.GetSheetNames meer dan .xls. Het accepteert ook de zip-gebaseerde .xlsx, .xlsm, .xltx en .xltm, waarbij alleen workbook.xml uit het archief wordt gehaald. Voor echte .xls-invoer scant het BoundSheet-records en stopt het bij het eerste EOF-record van de globals-substream, dus een groot binair bestand kost nog steeds alleen zijn openingskilobytes. De XLSX-facade draagt een garantie die meer betekent voor langlopende servicecode dan het op het eerste gezicht lijkt: TXLSXWorkbook.GetSheetNames laat de workbook-instantie noch resetten noch vullen, dus een instantie die al een geopend document vasthoudt, kan andere bestanden onderzoeken zonder het bestand in handen te storen. GetODSSheetNames past dezelfde aanpak toe op OpenDocument-pakketten, en elk van deze aanroepen heeft een stream-overload, waarmee je een upload kunt inspecteren die nooit op de schijf terechtkomt

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;

Dezelfde aanroep maakt een goede desktop-importdialoog. Toon de bladen, laat de gebruiker er één kiezen, en betaal pas voor de volledige open nadat de keuze is gemaakt. Bij een werkmap met vijftig bladen is het verschil zichtbaar: een kiezer die meteen verschijnt versus één die stokt terwijl het hele bestand erachter laadt

Macro-ingeschakelde .xlsm-bestanden en de sjabloonformaten geven exact dezelfde lijst als een gewoon .xlsx, aangezien de catalogus in dezelfde workbook.xml zit, ongeacht of er een vbaProject.bin meelift in het pakket. Een intakepipeline kan daarom de bladen van een macroworkbook opsommen voor routering, zonder ooit de macropayload aan te raken en zonder ooit iets te doen dat hem zou uitvoeren, en de macrobeleidsbeslissing overlaten aan de fase die het bestand daadwerkelijk opent

De returnwaarde lezen zonder jezelf voor de gek te houden

Returnconventies zijn niet uniform binnen HotXLS. Sommige aanroepen geven 1 terug bij succes, andere geven een aantal terug, dus voor de lijstfuncties is de enige controle die standhoudt elke waarde van nul of lager als mislukking behandelen, met de stringlijst geleegd. Weersta de verleiding om een lege lijst te lezen als "een werkmap zonder bladen." Zowel ECMA-376 als de BIFF8-specificatie vereisen ten minste één blad in een geldige werkmap, dus nul namen betekent altijd dat het lezen is mislukt, nooit dat het bestand legitiem leeg is

Een mislukte lijst is zelf een signaal dat het waard is te bewaren. Een .xlsx-bestand dat de aanroep laat mislukken, is één van een paar specifieke dingen: afgekapt, helemaal geen echt OOXML-pakket (verkeerd gelabelde CSV-exports uit andere systemen duiken hier voortdurend op), of een versleutelde container. Die uit elkaar houden is de taak van de volgende controle. Het loggen van de eerste bytes van het geweigerde bestand naast de mislukking, verandert een supportthread meestal in één enkel bericht

Versleutelde containers detecteren vóórdat je routeert

Een versleuteld .xlsx is geen zip. Het is een OLE-compound-bestand dat EncryptionInfo- en EncryptedPackage-streams omwikkelt, dus GetSheetNames kan er niet in kijken en geeft mislukking terug zoals elk ander onleesbaar bestand. CanReadEncrypted test op die containervorm, waardoor intake een versleuteld bestand doelbewust kan routeren in plaats van een generieke leesfout ergens diep in een worker te slikken:

Delphi-intaketriagestroom met HotXLS CanReadEncrypted en GetSheetNames die uploads routeren naar needs-password, onleesbaar of normaal
CanReadEncrypted draait eerst omdat een versleuteld OOXML-bestand een OLE-container is waarin de opsomm-aanroepen niet kunnen kijken
type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // Versleuteld OOXML is een OLE-container, geen zip: controleer eerst,
    // want de lijstaanroepen kunnen er niet in kijken.
    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;

Versleuteling is waar HotXLS bewust asymmetrisch is, dus de routering moet dat respecteren. Legacy .xls-versleuteling (RC4, RC4 CryptoAPI, XOR) is leesbaar: TXLSWorkbook.Open(FileName, Password) ontsleutelt met een opgeslagen wachtwoord, en die bestanden kunnen op het geautomatiseerde pad blijven. Versleutelde OOXML-pakketten gaan de andere kant op. HotXLS kan er één schrijven met SaveAsEncrypted, maar kan er geen terug inlezen. OpenEncrypted werpt EXlsxEncryptionNotImplemented op wanneer het een versleuteld pakket krijgt, en dat is waarom een eerlijk intakeontwerp versleuteld .xlsx naar een mens met Excel stuurt en het wachtwoorddragende .xls in code houdt

Voor batchwerk verdient deze classifier zijn plek door over een hele binnenkomende map te lopen voordat een worker met echte verwerking begint, aangezien elke probe ongeveer één bestandsopening en een paar kilobytes aan leeswerk kost. Het naar voren halen ervan verandert het faalscenario waar operations daadwerkelijk om geeft. In plaats van een taak om 3 uur 's nachts die sneuvelt op bestand 412 van 600, krijg je 412 bestanden in de wachtrij en 5 geweigerd bij intake met bij elk een reden. Dezelfde bibliotheekaanroepen, een veel beter operationeel verhaal

De vragen die een lijstaanroep niet kan beantwoorden

Namen en volgorde zijn het geheel van wat je krijgt. De lijstaanroepen zeggen niets over zichtbaarheid, dus verborgen en zeer-verborgen bladen komen in de lijst terecht en zien er hetzelfde uit als elk ander. Ze melden geen used-range-afmetingen, geen celaantallen en geen documenteigenschappen. Het docProps/core.xml-deel is ook klein, maar er is vandaag geen probe die alleen eigenschappen ophaalt, dus auteurs- en titelmetadata kosten nog steeds een volledige Open. De schone manier om daarmee te leven is de goedkope feiten elk bestand te laten routeren en de dure te reserveren voor bestanden die de routering overleven. Voor de bestanden die wel doorgaan naar een diepe lezing, draait een alleen-lezen scan van een groot .xls merkbaar sneller met _DisableGraphics := True, wat OfficeArt-parsing overslaat. Sla alleen nooit op vanuit die instantie: de tekenlaag die het oversloeg is verdwenen uit het model, en opslaan zou hem uit het bestand laten vallen

Bestanden die de triage doorstaan gaan meestal naar diepere analyse. De workbook-audit- en conversiewerkbank behandelt de per-blad-tellers die het waard zijn om te verzamelen zodra een volledige open gerechtvaardigd is, en de handleiding voor prestaties van grote werkmappen behandelt hoe je die volledige open snel houdt

HotXLS is een native Object Pascal-spreadsheetbibliotheek voor Delphi en C++Builder; het complete API-oppervlak, inclusief de inspectieaanroepen die hier zijn getoond, staat gedocumenteerd op de productpagina van HotXLS Delphi Component