Lassen Sie eine Batch-Konvertierung über zehntausend Tabellenkalkulationen über Nacht laufen, und am Morgen kommen drei davon mit False zurück. Das ist die gesamte Obduktion, die Ihnen ein boolesches Speicherergebnis liefert: eine Anzahl von Fehlschlägen, ohne jede Information darüber, welche Datei, welches Blatt oder welche von einem Dutzend möglicher Ursachen verantwortlich war. HotXLS, losLabs native Delphi- und C++Builder-Komponente für Excel-Dateien, ersetzt dieses eine Bit durch strukturierte Diagnostik. Die Schnittstelle IXLSWorkbookProgress stellt eine Diagnostics-Liste und ein OnDiagnostic-Ereignis bereit, die für jeden Open-, SaveAs- und Recalculate-Aufruf einen stabilen numerischen Code, einen Schweregrad, die fehlgeschlagene Operation und das Blatt melden, auf dem es passierte
Warum scheitert ein boolesches Speicherergebnis im großen Maßstab?
Eine einzelne fehlgeschlagene Datei ist nicht das Problem, das ein boolesches Ergebnis erzeugt; tausend davon sind es. Wenn SaveAs bei drei von zehntausend Dateien etwas anderes als Erfolg zurückgibt, ist die nächste Frage immer dieselbe: Sind diese drei wiederholbar, oder brauchen sie einen Menschen? Ein Berechtigungsfehler auf einer Netzwerkfreigabe ist nicht derselbe Vorfall wie eine Formel, die die Berechnungs-Engine nicht auswerten kann, und keines von beidem ist dasselbe wie ein Arbeitsblatt, das still eine Formatgrenze überschritten hat. Mit nur einem Bestanden/Nicht-bestanden-Ergebnis zum Arbeiten wird jedes davon zu einem identischen Support-Ticket, und jemand muss jede Datei von Hand öffnen, in Excel, und sie anstarren, bis die Ursache offensichtlich wird. Diese manuelle Triage ist die eigentlichen Kosten einer booleschen API, und sie skaliert linear mit der Größe des Batches, was genau die Eigenschaft ist, die man sich bei Fehlerbehandlung nicht wünscht
Innerhalb von IXLSWorkbookProgress: Was ein TXLSDiagnostic trägt
IXLSWorkbookProgress ist die Schnittstelle, die HotXLS verwendet, um sowohl zu melden, wie eine Operation voranschreitet, als auch, was dabei schiefging, und die beiden Hälften teilen sich aus einem Grund einen Vertrag: Beides sind Dinge, die ein lang laufender Open-, SaveAs- oder Recalculate-Aufruf kommunizieren muss, ohne mitten in der Operation eine Exception auszulösen. Die Fortschritts-Hälfte ist OnProgress und OnProgressEx, die mit einer Phase, einem Zustand und einem Aktuell/Gesamt-Paar feuern. Die Diagnostik-Hälfte ist die, um die es in diesem Artikel geht: eine Diagnostics-Eigenschaft, die eine TXLSDiagnostics-Liste zurückgibt, eine LastDiagnostic-Abkürzung für den jüngsten Eintrag, und ein OnDiagnostic-Ereignis, das in dem Moment feuert, in dem jeder TXLSDiagnostic-Datensatz erzeugt wird. Jeder Datensatz trägt einen numerischen Code, eine TXLSDiagnosticSeverity, die TXLSDiagnosticOperation, die ihn erzeugt hat, eine menschenlesbare Message, einen SheetIndex und SheetName, sowie einen NativeCode, der bewahrt, welcher niedrigere Rückgabewert auch immer den Eintrag ausgelöst hat
var
Book: TXLSXWorkbook;
Diag: TXLSDiagnostic;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.SaveAs('quarterly-report.xlsx') <> 1 then
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
Writeln(Format('[%d] severity=%d sheet="%s": %s',
[Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
end;
finally
Book.Free;
end;
end;
Diagnostics so zu lesen schlägt bereits von sich aus ein boolesches Ergebnis, weil Code und SheetName ein Rätsel in eine konkrete, filterbare Tatsache verwandeln. Der TXLSDiagnostic-Datensatz reicht weiter, als dieses Beispiel ausgibt: RecordId und StreamOffset existieren für die Byte-Ebenen-Forensik innerhalb eines BIFF-Streams, und PartName hält den OOXML-Zip-Eintrag, etwa xl/worksheets/sheet3.xml, aus dem ein Problem stammte. Wissenswert, bevor Sie Tooling darum herum bauen: In der aktuellen Version füllt keine der eingebauten Diagnostik-Aufrufstellen RecordId oder StreamOffset, sodass beide bei ihrem Konstruktor-Standardwert -1 bleiben, was "nicht zutreffend" statt "null" bedeutet. Behandeln Sie ihre Abwesenheit als normal, nicht als Fehler in Ihrem Handler
Zwei Engines, eine Form, ein leiser Unterschied
HotXLS liefert zwei Engines hinter diesem selben Berichtsmodell aus, eine BIFF8-Fassade für Legacy-.xls-Dateien und eine OOXML-Fassade für .xlsx, und beide legen IXLSWorkbookProgress nicht identisch frei. TXLSWorkbook, die .xls-Engine, implementiert IXLSWorkbookProgress formal, sodass sie überall übergeben werden kann, wo dieser Schnittstellentyp erwartet wird. TXLSXWorkbook, die .xlsx-Engine, legt dieselben Member Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress und OnProgressEx mit identischen Namen und Typen frei, aber als schlichte Klasse statt als formale Implementierung dieser Schnittstelle, sodass sie einen IXLSWorkbookProgress-Parameter nicht von sich aus erfüllt. In der Praxis spielt das selten eine Rolle, da die meiste Code-Basis jeweils gegen eine konkrete Workbook-Klasse arbeitet, aber es bedeutet, dass Sie keinen einzelnen Helfer schreiben können, der auf IXLSWorkbookProgress typisiert ist und ihm austauschbar das Workbook-Objekt einer der beiden Engines übergeben kann. Der eine Feldunterschied, der direkt aus der Formattrennung folgt, ist PartName: Nur die XLSX-Engine füllt ihn, weil nur OOXML Zip-Teile zu benennen hat
Was macht einen Diagnostik-Code zu etwas, auf das man sicher verzweigen kann?
Das Feld Code ist der einzige Teil einer Diagnose, gegen den es sich lohnt, einen Vergleich fest zu verdrahten; Message ist es nicht, denn Prosa ist genau die Art von Ding, die in einer späteren Version umformuliert, neu übersetzt oder mit mehr Detail erweitert wird, ohne dass irgendjemand das als Breaking Change behandelt. HotXLSs eingebaute Diagnostik-Codes lesen sich bereits so, als wären sie mit dieser Unterscheidung im Hinterkopf entworfen worden: speicherbezogene Codes laufen von 1000 bis 1005, öffnungsbezogene Codes sitzen bei 1100 und 1101, berechnungsbezogene Codes bei 1200 und 1201, und ein Code für nicht unterstütztes Format bei 1300, mit Lücken innerhalb jedes Bandes, statt dass die Codes über alle hinweg fortlaufend wären. Dieser Abstand ist es, der es einem Anbieter erlaubt, einen neuen Fehlschlagmodus zur Speicherzeit etwa bei 1006 hinzuzufügen, ohne die Codes umzunummerieren, von denen Ihre switch-Anweisung bereits abhängt, und es lohnt sich, das bei jeder Diagnostik-API zu prüfen, bevor Sie sich in der Produktion auf einen Code-Abgleich festlegen, nicht nur bei diesem hier. Behalten Sie in Ihrer eigenen Dispatch-Logik unabhängig davon, wie stabil die Nummerierung aussieht, einen Default-Zweig, denn neue Fehlschlagmodi sind genau das, was ein sich weiterentwickelnder Parser oder Writer ständig entdeckt. NativeCode und ExceptionClass sitzen eine Ebene unter Code, für den Fall, dass Sie eskalieren müssen: NativeCode bewahrt den zugrunde liegenden Rückgabewert, darunter ein HRESULT aus einem Structured-Storage-Aufruf, und ExceptionClass protokolliert den Delphi-Exception-Typ, wenn einer beteiligt war, was üblicherweise ausreicht, um eine präzise Support-Anfrage zu eröffnen, ohne einen vollständigen Stack-Trace anzuhängen
Schweregrad und Operation entscheiden, was Ihr Code als Nächstes tut
Schweregrad und Operation sind es, die aus einer Diagnose eine Log-Zeile in eine Routing-Entscheidung verwandeln. TXLSDiagnosticSeverity läuft über Info, Warning, Error und Fatal, und TXLSDiagnosticOperation markiert jeden Eintrag mit dem Aufruf, der ihn erzeugt hat: Open, Save, Calculate oder Export. Die beiden Achsen sind absichtlich unabhängig: xlsDiagnosticUnhandledException ist ein fester Code, der feuert, wobei Operation auf den Aufruf gesetzt wird, der ihn tatsächlich ausgelöst hat, sodass Code beantwortet, was schiefging, während Operation separat beantwortet, wo, statt einen eigenen Code für eine Exception beim Öffnen gegenüber einer beim Speichern zu brauchen. Diese Komponierbarkeit ist auch das, was Routing mechanisch macht: eine Warnung protokollieren und weitermachen, ein durch das Aborted-Flag abgebrochenes Speichern ist ein typisches Beispiel; einen Fehler zählen und den Batch weiterlaufen lassen, ein Arbeitsblatt, das nicht serialisiert werden konnte, ist ein typisches Beispiel; den Batch bei einem fatalen Schweregrad stoppen, weil diese Stufe bedeutet, dass eine unbehandelte Exception den Aufruf bereits abgewickelt hat und ein Fortfahren riskiert, von einem halb aktualisierten Zustand aus zu arbeiten. Ein ehrlicher Vorbehalt: Info existiert in der Aufzählung als der Standardwert, mit dem ein frisches TXLSDiagnostic beginnt, aber jede in die heutige HotXLS-Version eingebaute Diagnostik-Aufrufstelle löst jemals nur Warning, Error oder Fatal aus; Info ist für zukünftige Verwendung reserviert, nicht etwas, das die Engine heute ausgibt
// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
case Diag.Severity of
xlsDiagnosticWarning:
Writeln(Format('WARN [%d] %s', [Diag.Code, Diag.Message]));
xlsDiagnosticError:
begin
Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
[Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
Inc(FailedSheetCount);
end;
xlsDiagnosticFatal:
raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
end;
end;
OnDiagnostic in eine Batch-Pipeline verdrahten
Diagnostics nach jedem Aufruf abzufragen funktioniert für eine einzelne Datei; es hört auf zu funktionieren, sobald Sie wieder bei diesem nächtlichen Batch von zehntausend Dateien sind, weil Diagnostics zu Beginn jedes Open-, SaveAs- und Recalculate-Aufrufs geleert wird. Lesen Sie es nach der dritten Datei in einer Schleife, sehen Sie nur die Diagnosen der dritten Datei; was auch immer die ersten zwei Dateien gemeldet haben, ist bereits verschwunden. OnDiagnostic löst das, indem es die Sammlung in einen Strom verwandelt: einmal abonnieren, bevor die Schleife beginnt, und derselbe Handler feuert für jede Datei, in Reihenfolge, wobei der Dateiname über ein Instanzfeld weiterhin im Geltungsbereich bleibt
type
TBatchConverter = class
private
FCurrentFile: string;
FFailedFiles: TStringList;
procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
end;
procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
if Diagnostic.Severity >= xlsDiagnosticError then
FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
[FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;
// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
FCurrentFile := FileNames[I];
if Book.Open(FCurrentFile) = 1 then
Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;
Was der Callback tatsächlich kostet
OnDiagnostic ist aus strukturellen Gründen billig: Es feuert nur, wenn bereits etwas schiefläuft, und Schiefgehen ist selten im Vergleich zur Anzahl der Zellen, Zeilen oder Arbeitsblätter, die eine Arbeitsmappe enthält. Das steht im Kontrast zu OnProgress und OnProgressEx, die routinemäßigen Fortschritt melden und von Anfang an um die Aufrufhäufigkeit herum entworfen werden mussten. HotXLS feuert Fortschritt auf Arbeitsblattebene einmal pro Blatt während Open und SaveAs, nicht einmal pro Zelle oder Zeile, was den Overhead pro Aufruf selbst bei Arbeitsmappen mit Millionen von Zellen klein hält; Recalculate geht weiter und drosselt sein eigenes Fortschrittsereignis auf ungefähr alle vier Prozent des Abhängigkeitsgraphen, sodass eine vollständige Neuberechnung Ihnen einen Herzschlag gibt, statt Ihren UI-Thread mit Ereignissen zu überfluten. Diagnostik brauchte nichts von dieser Drosselung, weil die Ereignisanzahl durch die Anzahl der tatsächlichen Probleme begrenzt ist, nicht durch die Größe der Datei
Der eine Ort, an dem Leistung immer noch von Ihnen abhängt, ist innerhalb des Handlers selbst. OnDiagnostic feuert synchron, auf dem Thread, der Open, SaveAs oder Recalculate ausführt, sodass ein blockierender Handler, ein synchrones Schreiben an einen entfernten Logging-Dienst zum Beispiel, Teil der Wanduhrzeit dieses Aufrufs wird. Für eine einzelne Datei ist das unsichtbar. Multipliziert über einen Zehntausend-Dateien-Batch ist es der Unterschied zwischen einem Job, der über Nacht fertig wird, und einem, der zur Mittagszeit noch läuft, also puffern Sie, was der Handler tun muss, und leeren Sie es asynchron, statt den langsamen Teil inline auszuführen
Strukturierte Diagnostik ist genau dort am wertvollsten, wo ein boolesches Ergebnis am schwächsten ist, in Workflows, die viele statt einer einzigen Datei berühren. Eine Arbeitsmappen-Audit- und Konvertierungs-Pipeline ist das klarste Beispiel: Statt ein bloßes Bestanden/Nicht-bestanden pro Datei aufzuzeichnen, hängen Sie die Diagnostics-Liste jeder Datei an ihren Audit-Datensatz an, und der Report sagt Ihnen nicht nur, was fehlgeschlagen ist, sondern warum, was das meiste dessen ist, was unser Artikel zum Aufbau einer Arbeitsmappen-Audit- und Konvertierungswerkbank von vornherein richtig zu machen versucht. Dieselbe Paarung von Fortschritt und Diagnostik gehört auch in jeden Workflow, der bereits Fortschrittsberichterstattung um ihrer selbst willen braucht, was genau das Terrain ist, das in unserem Leitfaden zur Leistung großer Arbeitsmappen in HotXLS behandelt wird, wo ein langer Open- oder SaveAs-Aufruf häufig genug ist, dass OnProgress bereits verdrahtet ist und OnDiagnostic eine natürliche, nahezu kostenlose Ergänzung daneben ist
Nichts davon erfordert Excel irgendwo in der Pipeline installiert, und nichts davon erfordert, eine generische Exception abzufangen und zu raten, was sie bedeutete. IXLSWorkbookProgress und seine Member Diagnostics, LastDiagnostic und OnDiagnostic sind Teil der Standard-HotXLS-Komponente für Delphi und C++Builder, zusammen mit der vollständigen Diagnostik-Code-Referenz und dem Rest der Open-, SaveAs- und Recalculate-Oberfläche, die dieser Artikel durchgegangen ist