Zurück zur Homepage

Biff (Binary Interchange Format)
Schon seit langem reizt es mich, hinter die intimsten Geheimnisse von Excel zu kommen, es sozusagen beim Liebesspiel zu beobachten. Was wäre dazu besser geeignet, als sich die Struktur anzuschauen, in der Excel seine Daten speichert,  sozusagen die Aminosäuresequenzen auf Fileebene zu analysieren und zu entschlüsseln?
Da man das Rad ja nicht jedesmal neu erfinden muss, begann ich also vor Jahren mit der Suche nach Infos im großen weiten Netz. Ich fand auch einiges zu diesem Thema, zu meiner Enttäuschung musste ich aber feststellen, das nichts, aber auch gar nichts mit der Realität übereinstimmte. Keinen einzigen beschriebenen Record konnte ich mit dem Hexeditor ausmachen und so wurde das Thema erst mal abgehakt.
Vor kurzem las ich über die Unterschiede, in der Daten in Files gespeichert werden. Von Little- und Bigendian war da die Rede. Die Tatsache an sich war mir seit langem bekannt, aber erst jetzt fiel es mir plötzlich wie Schuppen aus den Haaren.  Das Low-Byte bei Intel und Co. ist vorne, also stimmt die Darstellung in den Dokumentationen mit einem Hex-Dump nicht überein. Sofort wurde der Hex-Editor angeschmissen und eine Excel-Datei angeschaut. Und siehe, es ward Licht!
Vom Licht der Erkenntnis, aber auch vom Schatten soll im folgenden die Rede sein:

Die Struktur von Excel in groben Zügen:
Eine Excel-Datei ist in viele verschiedene Abschnitte unterteilt.
Jeder Abschnitt repräsentiert ein Objekt, das kann ein Chart, ein Worksheet, ein VBA Blatt oder auch ein Workbook sein.
Jeder Abschnitt besteht aus vielen Records, die jeweils eine Eigenschaft, einen (Zell) Wert, eine Formel oder ein Format enthalten. Jeder Record besitzt einen Header, der immer gleich aufgebaut ist. Die ersten beiden Bytes identifizieren den Record, die nächsten zwei enthalten die Länge der folgenden Daten.
Die einzelnen Abschnitte beginnen mit einem BOF-Record und enden mit einem EOF-Record. Der BOF-Record zeigt gleichzeitig an, um welche Objektart es sich in diesem Abschnitt handelt.
Der Abschnitt „Workbook/Global“ sollte an erster Stelle stehen, dahinter kommen die Tabellenblätter, dann Charts, VBA etc.. Die Position des ersten BOF’s  ist ein Offset für verschiedene andere Zeiger, so dass man sich diesen merken sollte. In diesem Objekt „Workbook/Global“ sind Einstellungen des Workbooks gespeichert, wie Mappenschutz, Kennwörter, Formate, benutzte Schriftarten aber auch Daten wie die Shared-Stringgtabelle und die XF-Tabellen.
Ganz wichtig sollen auch die Boundsheet-Records sein, diese enthalten Namen, Typ und Streampositionen der einzelnen Sheets. Zu den Streampositionen muss als Offset die erste BOF-Position addiert werden und schon sollte man den BOF des gewünschten Tabellenblatts haben.
In diesem „Blatt“ Objekt gibt es ziemlich zu Beginn einen IndexRecord, der Infos über die erste/letzte benutzte Zeile und erste/letzte benutzte Spalte enthält. Dieser enthält auch einen Zeiger (plus 1. BOF) auf den DBCell-Record.
Der DBCell-Record enthält einen Zeiger rückwärts auf den ersten Row-Record, der Infos wie Zeilenhöhe- und Formate enthält. Darüber hinaus enthält der DBCell-Record als Array die Offsets zu den jeweils ersten Zellen einer Zeile, bezogen auf den Beginn des zweiten Row-Records.
Kompliziert genug? Es kommt noch besser!
In den Zell-Records sind die Daten nicht immer direkt gespeichert. Strings sind beispielsweise in der Shared-String Tabelle gespeichert, die Zell-Records enthalten nur den Index darauf. Formate sind ein Index auf die XF Tabelle, die gemeinsam benutzte Formate enthält.
Formeln werden nicht im Klartext gespeichert, wie er in Excel zu sehen ist, sondern als Token-Sequenz.
Die einfache Formel
 =A1+B1
steht dort etwa so:
(Token, um Operand auf Stack zu legen)(ZeileSpalte A1)  
(Token, um Operand auf Stack zu legen)(ZeileSpalte B1)   
(Token Operation) (Opcode +)

Die Funktion „Datum“ mit der Funktionsnummer 0041h ist in meiner Klasse umgesetzt, andere können durch Probieren ermittelt werden. Falls eine Funktion unbekannt ist, wird von meiner Klasse ein Hex-Dump geliefert.
Besondere Schwierigkeiten bereiten Zellen mit Zahlen als Inhalt.
IEEE Gleitpunktzahlen entsprechen noch einem Double, weshalb ein einfaches Kopieren auf ein Double mittels der API-Funktion CopyMemory klappt.
Die Probleme beginnen bei den platzsparenden RK-Zahlenformaten, die von Excel bevorzugt verwendet werden. Diese sind 4 Bytes lang, wobei 2 Bits davon die Art der Zahlen kennzeichnen.
Es gibt zum einen Gleitkommazahlen mit 30 Bit Länge (18 Bit Mantisse, 11 Bit Exponent, 1 Vorzeichenbit) und es gibt 30 Bit Vorzeichenbehaftete Ganzzahlen. Die Gleitkommazahlen lassen sich  nicht mittels CopyMemory in ein Single überführen und auch die Ganzzahlen bedürfen einer besonderen Behandlung. Ich habe viele Stunden damit verbracht, die Probleme dabei (vermeintlich) zu lösen. So ganz sicher, dass das unter allen Umständen  funzt, bin ich mir aber trotzdem nicht.

Weiter oben habe ich öfter das Wort soll benutzt. Warum?
Ganz einfach, Excel hält sich an keine Konventionen. Nach einer erneuten Speicherung ist plötzlich nicht mehr der BOF „Workbook/Global“ an erster Stelle, sondern ein ganz anderer. Ein Abschnitt am Ende enthält möglicherweise keinen EOF mehr. Dieser, und ein Teil der Daten beginnen dann schon mal vor dem ersten BOF. Die Zeiger, die eigentlich ein Schlüssel für hohe Verarbeitungsgeschwindigkeit sind, kann man dann meistens  vergessen. Sie stimmen einfach nicht mehr. Wenn man mit diesen Zeigern einen Zellwert sucht, dann gute Nacht. Viel Mühe musste ich aufwenden, um trotzdem an die Daten zu kommen. Ich kann mir vorstellen, dass sogar Excel, der Verursacher, damit Probleme bekommen kann. Zumindestens die Performance dürfte darunter nachhaltig leiden, auch wenn Excel so fehlertolerant ist, mit dem selbst erzeugten Mist noch klarzukommen.
Die vorliegende Mappe soll aufzeigen, wie man eine Arbeitsmappe analysieren kann. Die Klassen und Funktionen sind nicht ausgiebig getestet und auch nicht optimiert. Sie können und werden sicher auch den einen oder anderen Fehler haben. Viele Recordarten sind gar nicht berücksichtigt und erscheinen als unbekannt, obwohl sie sehr wohl bekannt sind, aber noch (oder auch nie) nicht umgesetzt sind. Viele Mappen bringen beim Parsen einen Fehler. Am besten eine neue Mappe angelegt, Werte eingetragen und geschaut was passiert. Das ist IMO der einzige Weg, den Aufbau wirklich zu verstehen.
Ich habe auch noch nicht begonnen, mich mit den VBA Modulen zu beschäftigen, es fehlt mir einfach etwas die Zeit.
Viel Spaß beim Probieren!

Beispieldatei (Biff.zip 163 KB)

P.S. Die Datei und der Code sind selbstverständlich nicht geschützt! 
Und auch hier gilt: Sie können den Code uneingeschränkt benutzen, eine Veröffentlichung ohne Rückfrage bitte ich, zu unterlassen.
Wenn euch die Homepage gefällt, hätte ich auch nichts gegen einen Eintrag ins Gästebuch einzuwenden.