HotXLS öffnet Arbeitsmappen, die von Excel 2.0, 3.0 und 4.0 geschrieben wurden, direkt aus Delphi und C++Builder. Diese Dateien stammen aus der Zeit vor dem OLE-Compound-Document-Container, den jede spätere .xls-Datei nutzt, sie sind also rohe BIFF-Datensatzströme ganz ohne Speicher-Wrapper, und ein für BIFF8 gebauter Reader findet darin keine einzige erkennbare Struktur. Das Öffnen erfolgt über denselben Open-Aufruf wie bei jeder anderen Arbeitsmappe; der Reader erkennt das Format und wechselt den Pfad
Die Dateien tauchen weiterhin auf, und das ist der einzige Grund, warum dies alles noch relevant ist. Technische Archive, behördliche Aufbewahrungspflichten, Labordaten von Messgeräten, deren Steuerungssoftware 1993 geschrieben wurde, und langlebige Buchhaltungssysteme haben allesamt BIFF2- und BIFF4-Arbeitsmappen hinterlassen. Modernes Excel weigert sich, mehrere davon überhaupt zu öffnen, da Legacy-Konverter aus Sicherheitsgründen entfernt wurden, wodurch ein Datenbestand entsteht, den niemand mit einem verfügbaren Werkzeug lesen kann
Was unterscheidet eine Arbeitsmappe aus der Zeit vor OLE?
Jede .xls-Datei ab Excel 5.0 ist eine OLE2-Compound-Datei, ein kleines Dateisystem innerhalb einer Datei, wobei die Arbeitsmappe in einem Stream namens Workbook oder Book lebt. Eine solche zu parsen beginnt mit dem Parsen dieses Containers, wie in dem Compound-File-Binärformat in Pascal beschrieben
BIFF2 bis BIFF4 haben keinen Container. Die Datei beginnt sofort mit einem BOF-Datensatz, und die Datensatznummer dieses BOF kodiert die Generation: $0009 für BIFF2, $0209 für BIFF3 und $0409 für BIFF4. HotXLS validiert die Länge des BOF-Rumpfs, die zwischen vier und sechs Bytes liegt, sowie den Substream-Typ – $0010 für ein Arbeitsblatt, $0020 für ein Diagramm und $0040 für ein Makroblatt –, bevor es sich auf den rohen Pfad festlegt. Diese Validierung verhindert, dass eine beschädigte oder falsch identifizierte Datei als sehr alte Arbeitsmappe interpretiert wird
Drei Generationen, drei Datensatz-Layouts
Die Zelldatensätze sind der Punkt, an dem sich die Generationen am sichtbarsten unterscheiden. BIFF2 belegt einen zusammenhängenden Block niedriger Datensatznummern, $0001 bis $0005 für leere, ganzzahlige, numerische, Beschriftungs- und boolesch-oder-Fehler-Zellen, und jeder Rumpf trägt ein Drei-Byte-Attributfeld, wo spätere Versionen einen erweiterten Formatindex platzieren. BIFF3 und BIFF4 geben das auf und nutzen stattdessen die Datensatznummern und Layouts von BIFF5, $0201, $0203, $0204 und $0205, mit einem Zwei-Byte-XF-Index
Dieses letzte Detail verursacht einen spezifischen und leicht falsch diagnostizierten Fehler. Ein BIFF3- oder BIFF4-LABEL-Datensatz ist strukturell identisch mit seinem BIFF5-Gegenstück, Zeile und Spalte gefolgt vom Formatindex und dann der Zeichenanzahl. Schreibt man einen Reader, der das BIFF2-Layout annimmt, liest er zwei Bytes zu wenig, läuft dann über das Ende des Datensatzes hinaus und interpretiert alles Nachfolgende falsch. Das Symptom ist keine Exception; es ist eine Arbeitsmappe, die sich mit plausibel wirkendem Datenmüll darin einlesen lässt
Formeldatensätze belegen über alle drei Generationen eine parallele Nummerierung, $0006, $0206 und $0406. Liefert eine Formel ein Zeichenkettenergebnis, kommt diese Zeichenkette in einem separaten, nachfolgenden Datensatz an, $0007 oder $0207, und die BIFF2-Form davon nutzt ein Einzelbyte-Längenpräfix statt des später verwendeten Zwei-Byte-Präfixes
Warum Formeln als Werte zurückkommen, nicht als Text
HotXLS liest das zwischengespeicherte Ergebnis einer Formel in diesen Dateien und versucht nicht, den Formelausdruck zu rekonstruieren. Dies ist eine bewusste Grenze, keine noch zu schließende Lücke
Der geparste Ausdruck in BIFF2 bis BIFF4 nutzt eine Token-Kodierung, die sich von BIFF5 und später auf eine Weise unterscheidet, die über Kosmetik hinausgeht: Token-Längen werden anders präfixiert, Referenz-Token haben andere Größen, und die Funktionsindex-Tabellen wurden zwischen den Generationen neu nummeriert. Diese Bytes durch einen BIFF8-Ausdrucksübersetzer laufen zu lassen erzeugt keine falsche Formel, sondern eine zufällige. Das zwischengespeicherte Ergebnis zu lesen liefert die Zahl oder Zeichenkette, die Excel zuletzt berechnet hat, was für eine Archivmigration tatsächlich benötigt wird
Der zwischengespeicherte Wert liegt an einem generationsabhängigen Offset innerhalb des Datensatzes: Byte 7 für BIFF2 und Byte 6 für BIFF3 und BIFF4. Sonderwerte – Zeichenketten, Boolesche Werte, Fehler und Leerzellen – werden in einem Markerwort $FFFF mit einem Unterscheidungsmerkmal kodiert, dieselbe Konvention, die spätere BIFF-Generationen beibehielten
Eine Datei öffnen
Der aufrufende Code ist unauffällig, und das ist der Punkt. Die Erkennung geschieht innerhalb von Open:
uses
lxHandle;
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
R, C: Integer;
V: Variant;
begin
Book := TXLSWorkbook.Create;
try
if Book.Open('archive\1993-inventory.xls') <> 1 then
begin
Writeln('unreadable - quarantine for manual review');
Exit;
end;
Sheet := Book.Sheets[1]; // Sheets[] ist 1-basiert
for R := Sheet.UsedRange.FirstRow + 1 to Sheet.UsedRange.LastRow + 1 do
for C := Sheet.UsedRange.FirstCol + 1 to Sheet.UsedRange.LastCol + 1 do
begin
V := Sheet.Cells[R, C].Value;
if not VarIsEmpty(V) then
Writeln(Format('R%dC%d = %s', [R, C, VarToStr(V)]));
end;
finally
Book.Free;
end;
end;
Man beachte die Indexarithmetik in dieser Schleife. UsedRange-Grenzen sind nullbasiert, während sowohl die Blattsammlung als auch der Zellzugriff einsbasiert sind, eine Inkonsistenz, die älter ist als die aktuelle API und aus Kompatibilitätsgründen erhalten bleibt. Wird die Anpassung vergessen, prüft man das falsche Rechteck und meldet dabei nichts Auffälliges. Günstige Vorprüfungen, die das Laden einer Datei ganz vermeiden, werden in leichtgewichtiger Arbeitsmappen-Inspektion behandelt
Was man nicht bekommt, und was dagegen zu tun ist
Formatierung wird nicht interpretiert. HotXLS parst die XF- und FONT-Datensätze dieser Generationen nicht, daher sind Schriftarten, Farben, Rahmen und Zahlenformate nicht verfügbar, und Zellen, die Excel einst als Datum anzeigte, kommen als ihre rohen fortlaufenden Zahlen zurück
Dieser letzte Punkt muss im eigenen Code behandelt werden, nicht im Reader, und der Grund ist ehrlich: Zahlenformate in BIFF2 bis BIFF4 sind nicht zuverlässig genug, um eine automatische Datumsentscheidung zu treffen. Eine Spalte fünfstelliger Zahlen könnte Daten sein, oder Teilenummern. Bewusst konvertieren, unter Verwendung des Datumssystems der Arbeitsmappe, dessen Regeln in Datumsseriennummern, das 1904-System und Zahlenformate beschrieben werden:
// Pro Spalte entscheiden, nie pro Wert: eine fünfstellige Zahl kann ein
// Datum oder eine Teilenummer sein, und das Legacy-Format verrät es nicht
if ColumnHoldsDates(C) then
begin
// Die beiden Datumssysteme liegen 1462 Tage auseinander, daher
// bezeichnet dieselbe Seriennummer zwei um vier Jahre versetzte Daten.
// Das System aus der Arbeitsmappe lesen, statt eines anzunehmen
if Book.Date1904 then
Writeln(DateToStr(SerialToDate1904(V)))
else
Writeln(DateToStr(SerialToDate1900(V)));
end
else
Writeln(VarToStr(V));
Zwei strukturelle Hinweise vervollständigen das Bild. Kennwortschutz- und Codepage-Datensätze erscheinen innerhalb des einzelnen Arbeitsblatt-Streams statt in einem Stream auf Arbeitsmappenebene, weil es keinen Stream auf Arbeitsmappenebene gibt, in den sie könnten, daher müssen sie im Arbeitsblattkontext erkannt werden. Und eine BIFF2- bis BIFF4-Datei enthält genau einen Blatt-Substream; mehrblättrige Arbeitsmappen gab es erst, als das Format seinen Container erhielt
Der pragmatische Migrationspfad ist daher zweistufig: die Legacy-Datei auf ihre Werte hin lesen, dann eine moderne Arbeitsmappe schreiben, die diese Werte mit selbst angewendeter Formatierung trägt. Legacy-Lesen, modernes Schreiben und alles dazwischen laufen in einer Bibliothek für Delphi und C++Builder, beschrieben auf der HotXLS-Delphi-Tabellenkalkulationskomponentenseite