Die Frage "definiert diese Arbeitsmappe noch einen Namen namens TaxRate?" beantwortet ein kleiner Teil eines XLSX-Pakets. Blattnamen, ihr Hidden- oder Very-Hidden-Zustand und jedes Arbeitsmappen-weite definierte Name leben in xl/workbook.xml (ECMA-376 Part 1, §18.2), meist ein paar Kilobytes. Die Zellen leben woanders, ein xl/worksheets/sheetN.xml-Part je Blatt plus eine Shared-String-Tabelle, und dort liegt fast alles an Bytes. Ein Loader, der darauf besteht, die komplette Arbeitsmappe zu bauen, bevor man irgendetwas fragen darf, inflatiert und parst alles davon, für jede Datei im Ordner, um eine Frage zu beantworten, die die ersten paar Kilobytes bereits geklärt haben
Die Aufteilung, die zählt, ist zwischen dem Lesen der Form einer Arbeitsmappe und dem Lesen ihres Gewichts. Ein Loader, der nach der Form aufhören, das Gewicht für eine gewählte Teilmenge von Blättern laden und ein weiteres Blatt später nachholen kann, macht aus "Arbeitsmappe öffnen", einem Alles-oder-Nichts-Schritt, etwas, das man je Aufruf begrenzt. Er bringt zwei Fallen mit, eine über Index-Basen und eine darüber, wie lange die Quelldatei still liegen bleiben muss
Was ein Metadata-only-Open liest und wo es anhält
Mit TXLSXWorkbook.MetadataOnly vor dem Open auf True gesetzt, parst HotXLS den Workbook-Part, Stile, Theme, definierte Namen, Blattnamen und Sichtbarkeits-Zustände, Verbindungen, externe Links und die Dokumenteigenschaften aus docProps/core.xml und docProps/app.xml. Danach überspringt es die Zell-XML jedes Arbeitsblatts und die Satelliten-Parts je Blatt: Kommentare, Zeichnungen und Tabellen. Die Shared-String-Tabelle wird ebenfalls übersprungen, was mehr wiegt, als es klingt, denn in Text-lastigen Arbeitsmappen kann dieser eine Part es mit den Worksheets an Größe aufnehmen
Die zurückkommende Arbeitsmappe ist strukturell vollständig. Sheets.Count stimmt, jedes Blatt hat seinen echten Namen und seine Visibility, DefinedNames ist gefüllt, ExternalLinks listet jedes verknüpfte Ziel, und Title, Author und LastModifiedBy tragen, was die Datei deklarierte. Nur hat jedes Blatt null Zellen
Genau das sind die Daten, auf denen ein Audit läuft. Welche Arbeitsmappen verstecken ein Blatt? Welche definieren noch TaxRate, und worauf? Welche verlinken auf eine File-Share, die im letzten Jahr eingestampft wurde? Der Scanner unten beantwortet alle drei für eine Datei. Beachten Sie die beiden Indexierungs-Konventionen nebeneinander: Sheets[] ist 1-basiert, passend zur XLS-Engine, während DefinedNames und ExternalLinks gewöhnliche 0-basierte Listen sind
procedure ScanWorkbook(const FileName: WideString; Report: TStrings);
var
Workbook: TXLSXWorkbook;
Name: TXLSXDefinedName;
I: Integer;
begin
Workbook := TXLSXWorkbook.Create;
try
Workbook.MetadataOnly := True; // wird von Open gelesen, also zuerst setzen
if Workbook.Open(FileName) <> 1 then
begin
Report.Add(FileName + ': open failed');
Exit;
end;
for I := 1 to Workbook.Sheets.Count do // Sheets[] ist 1-basiert
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 // 0-basierte Liste
Report.Add(FileName + ': links to ' + Workbook.ExternalLinks[I].Target);
finally
Workbook.Free;
end;
end;
Der Scanner liest mit Absicht Visibility statt des Booleans Visible. Visible ist für beide Hidden-Zustände False, und ein sehr verstecktes Blatt ist dasjenige, das ein Audit am meisten markieren will, denn Excels Einblenden-Dialog listet es nicht auf, und nur VBA oder eine direkte Änderung der Datei holt es zurück
Zwei von vierzig Blättern mit SelectedSheets laden
SelectedSheets ist eine TStringList, die der Arbeitsmappe gehört. Enthält sie mindestens einen Namen, parst Open die Zell-XML nur für die Blätter, die sie nennt; jedes andere Blatt wird mit seinem korrekten Namen und seiner Sichtbarkeit und ohne Zellen erzeugt. Eine leere Liste, der Standard, lädt jedes Blatt. Namen werden über den eigenen IndexOf der Liste nachgeschlagen, also ignoriert die Übereinstimmung bei Standardeinstellungen die Groß-/Kleinschreibung, was zu Excels Regel passt, dass zwei Blattnamen in einer Arbeitsmappe sich nicht nur in der Groß-/Kleinschreibung unterscheiden dürfen
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 ist Ihr eigener UI-Code
Die beiden Schalter wirken nur in eine Richtung zusammen. MetadataOnly = True überstimmt die Auswahl und überspringt alle Blätter, also kommt eine Arbeitsmappe, mit beiden konfiguriert, ganz ohne Zellen zurück, und die Auswahl wirkt kaputt, obwohl sie schlicht überstimmt wurde. Einen je Open verwenden: Metadata-only für die Entdeckung, eine Auswahl für die gezielte Arbeit
Selektives Laden läuft auch durch die parallele Parse-Phase, wenn ParallelParse an ist; nicht gewählte Blätter werden innerhalb der Worker-Jobs übersprungen, also verbringen die Threads ihre Zeit mit den Blättern, nach denen Sie fragten. Dasselbe gilt für die beiden Eigenschaften, wenn die Quelle eine binäre .xlsb-Arbeitsmappe ist, mit einer Geltungs-Regel: Eine .xlsb, die ein VBA-Projekt trägt, muss ohne diese beiden Optionen geöffnet werden; mit einer von beiden gesetzt meldet Open den selektiven Lesevorgang als nicht unterstützt
Warum lädt LoadSheet(1) das falsche Blatt?
Weil LoadSheet eine 0-basierte Position nimmt, während Sheets[] 1-basiert ist. LoadSheet(1) materialisiert das zweite Blatt der Arbeitsmappe, und da das Laden eines Blattes, das bereits Zellen hat, ein No-op ist, der True zurückgibt, kann ein verdrehter Index nach Erfolg aussehen, während das Blatt, auf das der Nutzer klickte, leer bleibt. Die Umrechnung gehört an genau eine Stelle in Ihrem Code
Die zweite Falle ist die Lebensdauer der Datei. LoadSheet hält das Archiv zwischen den Aufrufen nicht offen. Es öffnet die Originaldatei erneut bei Namen, parst den Workbook-Part, Stile und Shared Strings, dann das angeforderte Arbeitsblatt. Bevor dieser Datei vertraut wird, vergleicht HotXLS ihre Größe und letzte Schreibzeit mit den bei Open erfassten Werten; hat in der Zwischenzeit ein anderer Prozess die Arbeitsmappe überspeichert, gibt LoadSheet False zurück, statt Zellen aus einer anderen Version in das Modell zu mischen, das man bereits hält
procedure TReportForm.ShowSheetNo(SheetNo: Integer); // SheetNo ist 1-basiert, wie Sheets[]
var
Sheet: TXLSXWorksheet;
begin
Sheet := FWorkbook.Sheets[SheetNo];
if Sheet.Cells.Count = 0 then
if not FWorkbook.LoadSheet(SheetNo - 1) then // LoadSheet will die 0-basierte Position
begin
StatusBar.SimpleText := 'The workbook changed on disk; reopen it';
Exit;
end;
Grid.ShowSheet(Sheet); // Ihr eigener Grid-Code
end;
Ein paar Konsequenzen folgen aus diesem Entwurf. Eine aus einem TStream geöffnete Arbeitsmappe hat keine Quelldatei, zu der man zurückkehren könnte, also gibt LoadSheet für eine Stream-geöffnete .xlsx False zurück; dasselbe gilt für eine verschlüsselte Arbeitsmappe, die vor dem Parsen in den Speicher entschlüsselt wird. Eine selektiv aus einem Stream geöffnete binäre .xlsb ist die Ausnahme, denn HotXLS lagert eine temporäre Kopie des Streams für die zurückgestellten Blätter ein. Und ein wirklich leeres Blatt wird bei jedem Aufruf neu gelesen, denn null Zellen ist auch, wie ein übersprungenes Blatt aussieht
Das Kostenmodell entscheidet, wie man es verwendet. Jeder LoadSheet-Aufruf parst die Shared-String-Tabelle neu, also zahlt das Materialisieren von zehn Blättern nacheinander diesen Part zehnmal. Für einen Viewer, in dem der Nutzer gelegentlich einen weiteren Tab öffnet, ist das ein feiner Preis. Wenn man den Satz der Blätter bereits kennt, stattdessen mit SelectedSheets erneut öffnen: ein Archiv-Durchlauf, ein Shared-String-Parse. Es gibt auch einen Inhaltsunterschied: LoadSheet bringt die Zellen, Werte, Formeln und Formate eines Blattes mit, während Kommentare, Zeichnungen und Tabellen mit einem Open ankommen, also gehören Blätter, deren Kommentare oder Tabellen Ihr Code liest, von vornherein in die Auswahl
Warum verändert ein Roundtrip Parts, die man nie angefasst hat?
Ein Speichern, das alles regeneriert, schreibt Bytes neu, die sich nie geändert haben. Das Theme dekomprimieren, ein Objektmodell bauen, es serialisieren, wieder komprimieren, und das Ergebnis unterscheidet sich vom Original, obwohl niemand das Theme editierte. Für VBA-Projekte und Pivot-Caches ist der Preis höher als ein verrauschtes Diff: Ein VBA-Projekt ist eine binäre Compound-Datei mit eigenen internen Streams, und ein Pivot-Cache hält einen Schnappschuss von Quelldaten, deren Felder die Pivot-Table-Definitionen per Position adressieren, also ist das Sicherste mit beiden, wenn niemand sie änderte, ihre Bytes in Ruhe zu lassen
PreserveRawParts, das auf True voreingestellt ist, lässt SaveAs die komprimierten Bytes unveränderter Parts direkt aus dem Quellarchiv kopieren. Das deckt das Theme, das VBA-Projekt, Verbindungen, Diagramme, eingebettete Medien, Druckereinstellungen, Pivot-Tables und Pivot-Cache-Definitionen und -Records ab, jedes nur, solange das Modell sagt, dass es nicht geändert wurde. Diese Parts werden unterwegs nie inflatiert oder neu deflatiert. Die Property auf False zu setzen erzwingt die volle Neu-Serialisierung jedes Parts, der Schalter, zu dem man greift, wenn die Ausgabe nur widerspiegeln soll, was das Objektmodell enthält
Der Raw-Copy-Pfad verwendet dieselbe Datei-Prüfung wie LoadSheet: Die Quelle muss weiterhin mit der bei Open gesehenen Größe und Zeitstempel existieren. Tut sie es nicht, serialisiert SaveAs diese Parts normal, und die Datei ist weiterhin vollständig. Zwei Fälle nehmen immer den Serialisierungs-Pfad: eine Arbeitsmappe, deren Quelle eine binäre .xlsb war, die beim Lesen nach XML konvertiert wird, und ein Speichern mit gesetztem StrictOOXML, bei dem sich die Part-Namespaces ändern
Das Speichern einer teilweise geladenen Arbeitsmappe braucht eine Entscheidung. Für eine .xlsb-Quelle verweigert SaveAs den Dienst, solange ein Blatt noch zurückgestellt ist, und verzeichnet "Load every deferred binary worksheet before saving" in Diagnostics. Für eine .xlsx eine selektiv geladene Arbeitsmappe als Lese-Sicht behandeln: Ein Stub-Blatt hat keine Zellen, die der Schreiber ausgeben könnte, also entweder erst für jeden Stub LoadSheet aufrufen oder ohne Auswahl erneut öffnen, bevor man editiert und speichert
Eine Discovery-Pipeline, die mit Absicht erneut öffnet
Das Muster, das trägt, hat zwei Durchläufe. Durchlauf eins öffnet jede Datei nur nach Metadaten und entscheidet. Durchlauf zwei öffnet nur die Dateien erneut, die passten, mit einer Blattauswahl, und arbeitet. Erneut zu öffnen wirkt verschwenderisch, bis man zählt, was Durchlauf eins vermieden hat: Das zweite Open ist das einzige, das Zellen parst, und es parst nur die Blätter, die Durchlauf eins wählte
procedure ProcessInvoiceFile(const FileName: WideString);
var
Probe, Workbook: TXLSXWorkbook;
Wanted: TStringList;
I: Integer;
begin
Wanted := TStringList.Create;
try
// Durchlauf 1: Entdeckung, keine Zellen
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; // nichts in dieser Datei ist Parsing wert
// Durchlauf 2: Zellen nur für die gewählten Blätter
Workbook := TXLSXWorkbook.Create;
try
Workbook.SelectedSheets.Assign(Wanted);
if Workbook.Open(FileName) = 1 then
PostInvoices(Workbook); // Ihr eigener Verarbeitungs-Code
finally
Workbook.Free;
end;
finally
Wanted.Free;
end;
end;
MetadataOnly nach dem Open zu löschen macht aus der Probe keine vollständige Arbeitsmappe. Das Flag wird beim Öffnen gelesen; es nachträglich zu ändern wirkt nur auf das nächste Open an diesem Objekt. Die Probe kann weiterhin Blatt für Blatt durch LoadSheet wachsen, und für ein oder zwei Blätter ist das vernünftig, aber eine Pipeline ist mit dem sauberen Neu-Öffnen besser bedient, das auch die Kommentare und Tabellen jedes Blattes mitbringt
Wo das bei den übrigen Hebeln für große Dateien hingehört
Selektives Laden kürzt, was man parst. Es tut nichts für die Kosten eines Blattes, sobald es geladen ist, und es kann nicht helfen, wenn der Job jede Zelle jedes Blattes braucht. Dieser Fall dreht sich um Speicherung, behandelt im Artikel zu Row-Block-Zellspeicherung und bereichsbasierten Stilen, und um Parse-Durchsatz, behandelt in parallelem XLSX-Parsing und dem Speicherallokator. Ist die Aufgabe ein reines Lesen ganz ohne Editing, überspringt der Streaming-Direct-Reader das Arbeitsmappen-Objektmodell vollständig und ist das bessere Werkzeug als jede Lade-Strategie, die eines baut
Das alles läuft innerhalb Ihrer eigenen Delphi- oder C++Builder-ausführbaren Datei, ohne Excel-Installation auf dem Server, der das Scannen erledigt. Unterstützte Formate, Editionen und Lizenzierung sind auf der HotXLS-Delphi-Spreadsheet-Komponentenseite aufgeführt