Articolo tecnico

Caricamento metadata-only e selettivo di XLSX con HotXLS

La domanda "questo workbook definisce ancora un nome chiamato TaxRate?" è risposta di una piccola parte di un pacchetto XLSX. I nomi dei fogli, il loro stato nascosto o very-hidden e ogni defined name a livello workbook vivono in xl/workbook.xml (ECMA-376 Part 1, §18.2), di solito qualche kilobyte. Le celle vivono altrove, una parte xl/worksheets/sheetN.xml per foglio più una tabella shared-string, e lì sta quasi tutto il byte. Un loader che pretende di costruire il workbook completo prima che possiate chiedere qualcosa gonfia e analizza tutto, per ogni file nella cartella, per rispondere a una domanda che i primi kilobyte avevano già deciso

La spaccatura che conta è tra leggere la forma di un workbook e leggerne il peso. Un loader che può fermarsi dopo la forma, caricare il peso per un sottoinsieme scelto di fogli, e prendere un foglio in più più tardi trasforma "aprire un workbook" da un passo tutto-o-nulla in qualcosa che modulate per chiamata. Porta con sé anche due trappole, una sulle basi degli indici e una su quanto a lungo il file sorgente deve stare fermo

Cosa legge un Open metadata-only, e dove si ferma

Con TXLSXWorkbook.MetadataOnly impostato a True prima di Open, HotXLS analizza la parte workbook, gli stili, il tema, i defined names, i nomi e gli stati di visibilità dei fogli, le connessioni, i collegamenti esterni e le proprietà del documento da docProps/core.xml e docProps/app.xml. Poi salta l'XML delle celle di ogni foglio e le parti satellite per foglio: commenti, disegni e tabelle. Anche la tabella shared-string viene saltata, il che conta più di quanto suoni, perché nei workbook ricchi di testo quella parte può rivaleggiare con i fogli in dimensione

Il workbook che torna è strutturalmente completo. Sheets.Count è giusto, ogni foglio ha il suo vero nome e Visibility, DefinedNames è popolato, ExternalLinks elenca ogni destinazione collegata, e Title, Author e LastModifiedBy portano ciò che il file dichiarava. Ogni foglio semplicemente ha zero celle

HotXLS MetadataOnly legge proprietà del workbook, nomi dei fogli, stili e collegamenti in un archivio XLSX Delphi, poi si ferma prima di qualsiasi XML di celle del foglio
MetadataOnly legge ogni parte strutturale dell'archivio e si ferma prima dell'XML delle celle dei fogli. Il workbook Delphi torna con nomi dei fogli, visibilità, defined names e stili, e zero celle

È esattamente il dato su cui gira un audit. Quali workbook nascondono un foglio? Quali definiscono ancora TaxRate, e su cosa? Quali puntano a una condivisione di file dismessa l'anno scorso? Lo scanner sotto risponde a tutte e tre per un file. Notate le due convenzioni di indicizzazione fianco a fianco: Sheets[] è 1-based, come il motore XLS, mentre DefinedNames e ExternalLinks sono liste ordinarie 0-based

procedure ScanWorkbook(const FileName: WideString; Report: TStrings);
var
  Workbook: TXLSXWorkbook;
  Name: TXLSXDefinedName;
  I: Integer;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Workbook.MetadataOnly := True;           // letto da Open, quindi impostatelo prima
    if Workbook.Open(FileName) <> 1 then
    begin
      Report.Add(FileName + ': open failed');
      Exit;
    end;
    for I := 1 to Workbook.Sheets.Count do   // Sheets[] è 1-based
      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   // lista 0-based
      Report.Add(FileName + ': links to ' + Workbook.ExternalLinks[I].Target);
  finally
    Workbook.Free;
  end;
end;

Lo scanner legge Visibility invece del Boolean Visible di proposito. Visible è False per entrambi gli stati nascosti, e un foglio very hidden è quello che un audit vuole più segnalare, dato che la finestra di dialogo Unhide di Excel non lo elenca e solo VBA o una modifica diretta del file lo riporta alla luce

Caricare due fogli su quaranta con SelectedSheets

SelectedSheets è una TStringList posseduta dal workbook. Quando tiene almeno un nome, Open analizza l'XML delle celle solo per i fogli che nomina; ogni altro foglio viene creato col suo nome e visibilità corretti e senza celle. Una lista vuota, il predefinito, carica ogni foglio. I nomi vengono cercati con l'IndexOf della lista stessa, quindi con le impostazioni predefinite la corrispondenza ignora le maiuscole, il che si allinea alla regola di Excel per cui due nomi di foglio in un workbook non possono differire solo per maiuscole

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 è il vostro codice UI

I due interruttori interagiscono in una sola direzione. MetadataOnly = True scavalla la selezione e salta tutti i fogli, quindi un workbook configurato con entrambi torna senza celle da nessuna parte, e la selezione sembra rotta quando è semplicemente stata scavalcata. Usatene uno per Open: metadata-only per la discovery, una selezione per il lavoro mirato

Il caricamento selettivo passa anche per la fase di parsing parallelo quando ParallelParse è attivo; i fogli non selezionati vengono saltati dentro i job dei worker, quindi i thread spendono il loro tempo sui fogli che avete chiesto. Le stesse due proprietà valgono quando la sorgente è un workbook binario .xlsb, con una regola di scope: un .xlsb che porta un progetto VBA deve essere aperto senza queste due opzioni; con una delle due impostata, Open riporta la lettura selettiva come non supportata

HotXLS SelectedSheets analizza le celle per due fogli nominati su quaranta in Delphi mentre LoadSheet materializza un foglio saltato dal file sorgente invariato
SelectedSheets analizza l'XML delle celle solo per i fogli nominati, lasciando il resto come stub vuoti che conservano nome e visibilità. LoadSheet materializza uno stub più tardi, purché il file sorgente sia ancora su disco e invariato

Perché LoadSheet(1) carica il foglio sbagliato?

Perché LoadSheet prende una posizione 0-based mentre Sheets[] è 1-based. LoadSheet(1) materializza il secondo foglio del workbook, e dato che caricare un foglio che ha già celle è un no-op che restituisce True, un indice confuso può sembrare un successo mentre il foglio su cui l'utente ha cliccato resta vuoto. La conversione sta in esattamente un punto del vostro codice

La seconda trappola è la vita del file. LoadSheet non tiene aperto l'archivio tra le chiamate. Riapre il file originale per nome, analizza la parte workbook, gli stili e le shared string, poi analizza il foglio richiesto. Prima di fidarsi di quel file, HotXLS confronta la sua dimensione e l'ora dell'ultima scrittura con i valori catturati alla Open; se nel frattempo un altro processo ha salvato sopra il workbook, LoadSheet restituisce False invece di mescolare celle di una versione diversa nel modello che già tenete

procedure TReportForm.ShowSheetNo(SheetNo: Integer);  // SheetNo è 1-based, come Sheets[]
var
  Sheet: TXLSXWorksheet;
begin
  Sheet := FWorkbook.Sheets[SheetNo];
  if Sheet.Cells.Count = 0 then
    if not FWorkbook.LoadSheet(SheetNo - 1) then      // LoadSheet vuole la posizione 0-based
    begin
      StatusBar.SimpleText := 'The workbook changed on disk; reopen it';
      Exit;
    end;
  Grid.ShowSheet(Sheet);                              // il vostro codice griglia
end;

Da quel design seguono alcune conseguenze. Un workbook aperto da un TStream non ha un file sorgente a cui tornare, quindi LoadSheet restituisce False per un .xlsx aperto da stream; lo stesso vale per un workbook cifrato, che viene decifrato in memoria prima del parsing. Un .xlsb binario aperto selettivamente da stream è l'eccezione, perché HotXLS mette in scena una copia temporanea dello stream per i fogli rimandati. E un foglio genuinamente vuoto verrà riletto a ogni chiamata, dato che zero celle è anche l'aspetto di un foglio saltato

Il modello dei costi decide come usarlo. Ogni chiamata LoadSheet ri-analizza la tabella shared-string, quindi materializzare dieci fogli uno per uno paga quella parte dieci volte. Per un viewer dove l'utente apre un'altra scheda ogni tanto, è un prezzo buono. Quando sapete già il set di fogli, riaprite con SelectedSheets invece: un passaggio d'archivio, un parsing di shared-string. C'è anche una differenza di contenuto: LoadSheet porta le celle di un foglio, valori, formule e formati, mentre commenti, disegni e tabelle arrivano con un Open, quindi i fogli i cui commenti o tabelle il vostro codice legge stanno nella selezione fin dall'inizio

Perché un round trip cambia part che non hai mai toccato?

Un salvataggio che rigenera tutto riscrive byte che non sono mai cambiati. Decomprimete il tema, costruite un modello a oggetti, serializzatelo, ricomprimetelo, e il risultato differisce dall'originale anche se nessuno ha modificato il tema. Per i progetti VBA e i pivot cache il costo è più alto di un diff rumoroso: un progetto VBA è un compound file binario con i suoi stream interni, e un pivot cache tiene uno snapshot di dati sorgente i cui campi le definizioni della pivot table indirizzano per posizione, quindi la cosa più sicura da fare con uno dei due, quando nessuno l'ha modificato, è lasciare i suoi byte in pace

PreserveRawParts, che vale True per predefinito, fa copiare a SaveAs i byte compressi delle parti invariate dritte dall'archivio sorgente. Copre il tema, il progetto VBA, le connessioni, i grafici, i media incorporati, le impostazioni di stampa, le pivot table e le definizioni e i record dei pivot cache, ognuno solo mentre il modello dice che non è stato modificato. Quelle parti non vengono mai gonfiate né ri-sgonfiate lungo la strada. Impostare la proprietà a False forza la ri-serializzazione completa di ogni parte, che è l'interruttore da afferrare quando volete che l'output rifletta solo ciò che il modello a oggetti contiene

Il percorso di copia grezza usa lo stesso controllo file di LoadSheet: la sorgente deve ancora esistere con la dimensione e il timestamp visti alla Open. Quando non è così, SaveAs serializza quelle parti normalmente e il file resta comunque completo. Due casi prendono sempre la strada della serializzazione: un workbook la cui sorgente era un .xlsb binario, che viene convertito in XML alla lettura, e un salvataggio con StrictOOXML impostato, dove i namespace delle parti cambiano

Salvare un workbook caricato parzialmente vuole una decisione. Per una sorgente .xlsb, SaveAs rifiuta finché qualche foglio è ancora rimandato e registra "Load every deferred binary worksheet before saving" in Diagnostics. Per un .xlsx, trattate un workbook caricato selettivamente come una vista di lettura: uno stub di foglio non ha celle da emettere per il writer, quindi o chiamate LoadSheet per ogni stub prima o riaprite senza selezione prima di modificare e salvare

Una pipeline di discovery che riapre di proposito

Il pattern che tiene in piedi ha due passaggi. Il passaggio uno apre ogni file metadata-only e decide. Il passaggio due riapre solo i file che hanno corrisposto, con una selezione di fogli, e fa il lavoro. Riaprire sembra sprecone finché non contate ciò che il passaggio uno ha evitato: la seconda Open è l'unica che analizza celle, e analizza solo i fogli che il passaggio uno ha scelto

Pipeline di discovery in due passaggi HotXLS in Delphi: un passaggio metadata-only decide quali workbook e fogli contano, poi un passaggio mirato analizza solo i fogli selezionati
Il passaggio uno scandisce la directory metadata-only e decide quali file e fogli contano; il passaggio due riapre le corrispondenze con una selezione di fogli. Solo la seconda open analizza mai l'XML delle celle
procedure ProcessInvoiceFile(const FileName: WideString);
var
  Probe, Workbook: TXLSXWorkbook;
  Wanted: TStringList;
  I: Integer;
begin
  Wanted := TStringList.Create;
  try
    // Passaggio 1: discovery, nessuna cella
    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;                               // niente in questo file vale la pena analizzare

    // Passaggio 2: celle solo per i fogli scelti
    Workbook := TXLSXWorkbook.Create;
    try
      Workbook.SelectedSheets.Assign(Wanted);
      if Workbook.Open(FileName) = 1 then
        PostInvoices(Workbook);           // il vostro codice di elaborazione
    finally
      Workbook.Free;
    end;
  finally
    Wanted.Free;
  end;
end;

Cancellare MetadataOnly dopo Open non trasforma il probe in un workbook completo. Il flag viene letto durante l'apertura; cambiarlo dopo influisce solo sulla prossima Open su quell'oggetto. Il probe può comunque far crescere un foglio alla volta tramite LoadSheet, e per uno o due fogli è ragionevole, ma una pipeline sta meglio con la riapertura pulita, che porta con sé anche i commenti e le tabelle di ogni foglio

Dove si incasta con le altre leve per file grandi

Il caricamento selettivo taglia ciò che analizzate. Non fa nulla per il costo di un foglio una volta caricato, e non può aiutare quando il job vuole ogni cella di ogni foglio. Quel caso riguarda lo storage, trattato nell'articolo su storage delle celle a blocchi di righe e stili per intervallo, e il throughput del parsing, trattato in parsing XLSX parallelo e allocatore di memoria. Quando il compito è una lettura pura senza alcuna modifica, il lettore diretto in streaming salta del tutto il modello a oggetti del workbook ed è lo strumento migliore di qualunque strategia di caricamento che ne costruisca uno

Tutto questo gira dentro il vostro eseguibile Delphi o C++Builder, senza installazione di Excel sul server che fa la scansione. Formati supportati, edizioni e licenze sono elencati nella pagina del componente foglio di calcolo Delphi HotXLS