Teknisk artikkel

Lese Excel 2.0- til 4.0-filer i Delphi med HotXLS

HotXLS åpner arbeidsbøker skrevet av Excel 2.0, 3.0 og 4.0 direkte fra Delphi og C++Builder. Disse filene er eldre enn OLE-sammensatt-dokument-beholderen hver senere .xls bruker, så de er rå BIFF-postrømmer helt uten lagringsomslag, og en leser bygget for BIFF8 vil ikke finne en eneste gjenkjennelig struktur inni dem. Å åpne én bruker det samme Open-kallet som enhver annen arbeidsbok; leseren oppdager formatet og bytter bane

Filene dukker fremdeles opp, og det er den eneste grunnen til at noe av dette betyr noe. Ingeniørarkiver, offentlig journalbevaring, laboratoriedata fra instrumenter hvis kontrollprogramvare ble skrevet i 1993, og langvarige regnskapssystemer har alle etterlatt seg BIFF2- og BIFF4-arbeidsbøker. Moderne Excel nekter å åpne flere av dem rett ut, siden gamle konvertere er fjernet av sikkerhetsgrunner, noe som etterlater et datasett ingen kan lese med et verktøy noen faktisk har

Hva gjør en pre-OLE-arbeidsbok annerledes?

Hver .xls fra Excel 5.0 og fremover er en OLE2-sammensatt fil, et lite filsystem inni en fil, der arbeidsboken bor i en strøm kalt Workbook eller Book. Å analysere én starter med å analysere den beholderen, som beskrevet i det sammensatte binærfilformatet i Pascal

BIFF2 til og med BIFF4 har ingen beholder. Filen starter umiddelbart med en BOF-post, og postnummeret til den BOF-en koder generasjonen: $0009 for BIFF2, $0209 for BIFF3 og $0409 for BIFF4. HotXLS validerer BOF-kroppslengden, som er mellom fire og seks byte, og substrømtypen, $0010 for et regneark, $0020 for et diagram og $0040 for et makroark, før den forplikter seg til den rå banen. Den valideringen er det som hindrer en korrupt eller feilidentifisert fil i å bli tolket som en veldig gammel arbeidsbok

Tre generasjoner, tre postoppsett

Celleposter er der generasjonene avviker mest synlig. BIFF2 opptar en sammenhengende blokk av lave postnumre, $0001 til og med $0005 for tomme, heltall-, tall-, etikett- og boolsk-eller-feil-celler, og hver kropp bærer et attributtfelt på tre byte der senere versjoner setter en utvidet formatindeks. BIFF3 og BIFF4 forlater det og gjenbruker BIFF5-postnumrene og -oppsettene, $0201, $0203, $0204 og $0205, med en XF-indeks på to byte

Den siste detaljen forårsaker en spesifikk og lett feildiagnostisert feil. En BIFF3- eller BIFF4-LABEL-post er strukturelt identisk med sin BIFF5-motpart, rad og kolonne etterfulgt av formatindeksen og deretter tegnantallet. Skriv en leser som antar BIFF2-oppsettet, og den leser to byte for lite, går deretter ut over slutten av posten og mistolker alt etter den. Symptomet er ikke et unntak; det er en arbeidsbok som leses med plausibelt søppel inni seg

Formelposter opptar en parallell nummerering på tvers av alle tre, $0006, $0206 og $0406. Når en formel produserer et strengresultat, kommer den strengen i en separat, etterfølgende post, $0007 eller $0207, og BIFF2-formen av den bruker et lengdeprefiks på én byte i stedet for det på to byte som brukes senere

Hvorfor formler kommer tilbake som verdier, ikke som tekst

HotXLS leser det bufrede resultatet av en formel i disse filene og forsøker ikke å rekonstruere formeluttrykket. Dette er en bevisst grense, ikke et hull som venter på å bli fylt

Det analyserte uttrykket i BIFF2 til BIFF4 bruker en token-koding som skiller seg fra BIFF5 og senere på måter som går utover det kosmetiske: tokenlengder prefikses annerledes, referansetokens har andre størrelser, og funksjonsindekstabellene ble omnummerert mellom generasjonene. Å kjøre de bytene gjennom en BIFF8-uttrykksoversetter produserer ikke en feil formel, den produserer en tilfeldig en. Å lese den bufrede verdien gir deg tallet eller strengen Excel sist beregnet, som er det en arkivmigrering faktisk trenger

Den bufrede verdien bor på en generasjonsavhengig forskyvning inni posten: byte 7 for BIFF2 og byte 6 for BIFF3 og BIFF4. Spesialverdier, strenger, boolske verdier, feil og tomme celler, kodes i et markerord på $FFFF med en diskriminator, den samme konvensjonen senere BIFF-generasjoner beholdt

Åpne en

Den kallende koden er ganske ordinær, og det er poenget. Deteksjon skjer inni 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[] er 1-basert
    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;

Legg merke til indeksaritmetikken i den løkken. UsedRange-grenser er nullbaserte, mens både arksamlingen og celletilgangen er enbaserte, en inkonsistens som er eldre enn dagens API og bevares for kompatibilitet. Å glemme justeringen reviderer det gale rektangelet og rapporterer ingenting uvanlig mens den gjør det. Billige forhåndssjekker som unngår å laste inn en fil i det hele tatt, er dekket i lettvekts arbeidsbokinspeksjon

Hva du ikke får, og hva du skal gjøre med det

Formatering tolkes ikke. HotXLS analyserer ikke XF- og FONT-postene til disse generasjonene, så skrifter, farger, kantlinjer og tallformater er utilgjengelige, og celler Excel en gang viste som datoer, kommer tilbake som sine rå serienumre

Den siste trenger håndtering i din egen kode snarere enn i leseren, og grunnen er ærlig: tallformater i BIFF2 til BIFF4 er ikke pålitelige nok til å styre en automatisk datobeslutning. En kolonne med femsifrede tall kan være datoer, eller kan være delenumre. Konverter bevisst, ved å bruke arbeidsbokens datosystem, hvis regler er beskrevet i dato-serienumre, 1904-systemet og tallformater:

// Avgjør per kolonne, aldri per verdi: et femsifret tall kan være en
// dato eller et delenummer, og det gamle formatet vil ikke fortelle deg det
if ColumnHoldsDates(C) then
begin
  // De to datosystemene ligger 1462 dager fra hverandre, så det samme serienummeret
  // angir to datoer fire år fra hverandre. Les systemet fra
  // arbeidsboken i stedet for å anta ett
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

To strukturelle merknader fullfører bildet. Passordbeskyttelse og kodesideposter opptrer inni den ene regnearkstrømmen snarere enn i en strøm på arbeidsboknivå, fordi det ikke finnes noen strøm på arbeidsboknivå å legge dem i, så de må gjenkjennes i regnearkkontekst. Og en BIFF2- til BIFF4-fil inneholder nøyaktig én arksubstrøm; flerarks-arbeidsbøker fantes ikke før formatet fikk sin beholder

Den pragmatiske migreringsbanen er derfor en totrinns en: les den gamle filen for verdiene dens, skriv deretter en moderne arbeidsbok som bærer de verdiene med formatering du påfører selv. Gammel lesing, moderne skriving og alt derimellom kjører i ett bibliotek for Delphi og C++Builder, beskrevet på HotXLS Delphi regnearkkomponentsiden