HotXLS opent werkmappen geschreven door Excel 2.0, 3.0 en 4.0 rechtstreeks vanuit Delphi en C++Builder. Deze bestanden dateren van vóór de OLE compound-document-container die elk latere .xls-bestand gebruikt, dus het zijn ruwe BIFF-recordstreams zonder enige opslagwrapper, en een lezer die is gebouwd voor BIFF8 vindt er geen enkele herkenbare structuur in. Het openen ervan gebruikt dezelfde Open-aanroep als elke andere werkmap; de lezer detecteert het formaat en schakelt over naar het juiste pad
Deze bestanden duiken nog steeds op, en dat is de enige reden waarom dit er allemaal toe doet. Technische archieven, bewaarplichten voor overheidsdossiers, laboratoriumgegevens van instrumenten waarvan de besturingssoftware in 1993 is geschreven, en langlopende boekhoudsystemen hebben allemaal BIFF2- en BIFF4-werkmappen achtergelaten. Moderne Excel weigert er meerdere ronduit te openen, omdat legacy-converters om veiligheidsredenen zijn verwijderd, wat een dataset achterlaat die niemand met een tool kan lezen die iemand heeft
Wat maakt een pre-OLE-werkmap anders?
Elk .xls-bestand vanaf Excel 5.0 is een OLE2-compound-bestand, een klein bestandssysteem binnen een bestand, waarbij de werkmap zich bevindt in een stream genaamd Workbook of Book. Het parsen van zo'n bestand begint met het parsen van die container, zoals beschreven in het compound-file-binaire formaat in Pascal
BIFF2 tot en met BIFF4 hebben geen container. Het bestand begint onmiddellijk met een BOF-record, en het recordnummer van die BOF codeert de generatie: $0009 voor BIFF2, $0209 voor BIFF3 en $0409 voor BIFF4. HotXLS valideert de BOF-body-lengte, die tussen vier en zes bytes ligt, en het substreamtype, $0010 voor een werkblad, $0020 voor een grafiek en $0040 voor een macroblad, voordat het zich vastlegt op het ruwe pad. Die validatie is wat voorkomt dat een corrupt of verkeerd geïdentificeerd bestand wordt geïnterpreteerd als een zeer oude werkmap
Drie generaties, drie recordindelingen
De celrecords zijn waar de generaties het meest zichtbaar uiteenlopen. BIFF2 neemt een aaneengesloten blok lage recordnummers in beslag, $0001 tot en met $0005 voor lege, geheeltallige, numerieke, label- en boolean-of-foutcellen, en elke body draagt een drie-byte attribuutveld waar latere versies een uitgebreide format-index plaatsen. BIFF3 en BIFF4 laten dat los en hergebruiken de BIFF5-recordnummers en -indelingen, $0201, $0203, $0204 en $0205, met een twee-byte XF-index
Dat laatste detail veroorzaakt een specifieke en makkelijk verkeerd gediagnosticeerde fout. Een BIFF3- of BIFF4-LABEL-record is structureel identiek aan zijn BIFF5-tegenhanger: rij en kolom gevolgd door de format-index en dan het tekenaantal. Schrijf een lezer die de BIFF2-indeling veronderstelt, en die leest twee bytes te weinig, loopt vervolgens voorbij het einde van het record en interpreteert alles erna verkeerd. Het symptoom is geen uitzondering; het is een werkmap die inleest met plausibele rommel erin
Formulerecords bezetten een parallelle nummering over alle drie de generaties heen, $0006, $0206 en $0406. Wanneer een formule een stringresultaat produceert, komt die string aan in een apart volgend record, $0007 of $0207, en de BIFF2-vorm daarvan gebruikt een lengteprefix van één byte in plaats van de tweebyte-versie die later wordt gebruikt
Waarom formules terugkomen als waarden, niet als tekst
HotXLS leest het gecachte resultaat van een formule in deze bestanden en probeert niet de formule-expressie te reconstrueren. Dit is een bewuste grens, geen leemte die wacht om opgevuld te worden
De geparste expressie in BIFF2 tot BIFF4 gebruikt een tokencodering die verschilt van BIFF5 en later op manieren die verder gaan dan cosmetisch: tokenlengtes worden anders geprefixt, verwijzingstokens hebben andere groottes, en de functie-indextabellen zijn tussen generaties hernummerd. Die bytes door een BIFF8-expressievertaler halen produceert geen verkeerde formule, het produceert een willekeurige. Het lezen van de gecachte waarde geeft u het getal of de string die Excel het laatst berekende, wat is wat een archiefmigratie daadwerkelijk nodig heeft
De gecachte waarde bevindt zich op een generatie-afhankelijke offset binnen het record: byte 7 voor BIFF2 en byte 6 voor BIFF3 en BIFF4. Speciale waarden, strings, booleans, fouten en lege cellen, worden gecodeerd in een markeerwoord van $FFFF met een discriminator, dezelfde conventie die latere BIFF-generaties behielden
Er een openen
De aanroepende code is onopvallend, en dat is precies het punt. Detectie gebeurt binnen 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[] begint bij 1
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;
Let op de indexrekenkunde in die lus. UsedRange-grenzen zijn nulgebaseerd, terwijl zowel de werkbladverzameling als celtoegang één-gebaseerd zijn, een inconsistentie die dateert van vóór de huidige API en om compatibiliteitsredenen behouden blijft. Het vergeten van de aanpassing controleert het verkeerde rechthoekige gebied en rapporteert daarbij niets ongewoons. Goedkope voorcontroles die het laden van een bestand helemaal vermijden, worden behandeld in lichtgewicht werkmapinspectie
Wat u niet krijgt, en wat u eraan kunt doen
Opmaak wordt niet geïnterpreteerd. HotXLS parst de XF- en FONT-records van deze generaties niet, dus lettertypen, kleuren, randen en getalnotaties zijn niet beschikbaar, en cellen die Excel ooit als datums weergaf, komen terug als hun ruwe seriële getallen
Dat laatste moet in uw eigen code worden afgehandeld in plaats van in de lezer, en de reden is eerlijk: getalnotaties in BIFF2 tot BIFF4 zijn niet betrouwbaar genoeg om een automatische datumbeslissing aan te sturen. Een kolom met vijfcijferige getallen kan datums zijn, of onderdeelnummers. Converteer bewust, met behulp van het datumsysteem van de werkmap, waarvan de regels worden beschreven in seriële datumgetallen, het 1904-systeem en getalnotaties:
// Beslis per kolom, nooit per waarde: een vijfcijferig getal kan een
// datum of een onderdeelnummer zijn, en de legacy-notatie vertelt het u niet
if ColumnHoldsDates(C) then
begin
// De twee datumsystemen liggen 1462 dagen uit elkaar, dus hetzelfde seriële
// getal duidt op twee datums die vier jaar uit elkaar liggen. Lees het systeem
// uit de werkmap in plaats van er een aan te nemen
if Book.Date1904 then
Writeln(DateToStr(SerialToDate1904(V)))
else
Writeln(DateToStr(SerialToDate1900(V)));
end
else
Writeln(VarToStr(V));
Twee structurele opmerkingen maken het plaatje compleet. Records voor wachtwoordbeveiliging en codepagina's verschijnen binnen de enkele werkbladstream in plaats van in een stream op werkmapniveau, omdat er geen stream op werkmapniveau is om ze in te plaatsen, dus moeten ze worden herkend binnen werkbladcontext. En een BIFF2- tot BIFF4-bestand bevat precies één werkbladsubstream; werkmappen met meerdere werkbladen bestonden niet totdat het formaat zijn container kreeg
Het pragmatische migratiepad is daarom een tweestappenpad: lees het legacy-bestand voor zijn waarden, schrijf dan een moderne werkmap die die waarden draagt met opmaak die u zelf toepast. Legacy lezen, modern schrijven en alles ertussenin draaien in één bibliotheek voor Delphi en C++Builder, beschreven op de HotXLS Delphi-spreadsheetcomponentpagina