Articol tehnic

Citirea fișierelor Excel 2.0-4.0 în Delphi cu HotXLS

HotXLS deschide registre de lucru scrise de Excel 2.0, 3.0 și 4.0 direct din Delphi și C++Builder. Aceste fișiere sunt anterioare containerului de document compus OLE pe care îl folosește orice .xls ulterior, așa că sunt fluxuri brute de înregistrări BIFF fără niciun înveliș de stocare, iar un cititor construit pentru BIFF8 nu va găsi nicio structură recunoscută în interiorul lor. Deschiderea unuia folosește același apel Open ca orice alt registru de lucru; cititorul detectează formatul și schimbă calea

Fișierele încă apar, și acesta este singurul motiv pentru care toate acestea contează. Arhive de inginerie, păstrarea evidențelor guvernamentale, date de laborator de la instrumente al căror software de control a fost scris în 1993, și sisteme contabile de lungă durată au lăsat toate în urmă registre de lucru BIFF2 și BIFF4. Excel modern refuză să deschidă mai multe dintre ele, deoarece a eliminat convertoarele vechi din motive de securitate, ceea ce lasă un set de date pe care nimeni nu-l poate citi cu un instrument pe care cineva îl are

Ce face un registru de lucru dinainte de OLE diferit?

Fiecare .xls de la Excel 5.0 încoace este un fișier compus OLE2, un mic sistem de fișiere în interiorul unui fișier, cu registrul de lucru locuind într-un flux numit Workbook sau Book. Analizarea unuia începe cu analizarea acelui container, așa cum este descris în formatul binar de fișier compus în Pascal

BIFF2 până la BIFF4 nu au container. Fișierul începe imediat cu o înregistrare BOF, iar numărul de înregistrare al acelui BOF codifică generația: $0009 pentru BIFF2, $0209 pentru BIFF3 și $0409 pentru BIFF4. HotXLS validează lungimea corpului BOF, care este între patru și șase octeți, și tipul de substream, $0010 pentru o foaie de lucru, $0020 pentru un grafic și $0040 pentru o foaie de macrocomenzi, înainte de a se angaja pe calea brută. Acea validare este ceea ce împiedică un fișier corupt sau greșit identificat să fie interpretat ca un registru de lucru foarte vechi

Trei generații, trei aranjamente de înregistrări

Înregistrările de celule sunt locul unde generațiile diverg cel mai vizibil. BIFF2 ocupă un bloc contiguu de numere de înregistrare mici, $0001 până la $0005 pentru celule blank, întreg, număr, etichetă și boolean-sau-eroare, iar fiecare corp poartă un câmp de atribute de trei octeți unde versiunile ulterioare pun un index de format extins. BIFF3 și BIFF4 abandonează asta și refolosesc numerele de înregistrare și aranjamentele BIFF5, $0201, $0203, $0204 și $0205, cu un index XF de doi octeți

Acel ultim detaliu cauzează un eșec specific și ușor de diagnosticat greșit. O înregistrare LABEL BIFF3 sau BIFF4 este identică structural cu echivalentul ei BIFF5, rând și coloană urmate de indexul de format și apoi numărul de caractere. Scrie un cititor care presupune aranjamentul BIFF2 și acesta citește cu doi octeți prea puțin, apoi merge dincolo de capătul înregistrării și interpretează greșit tot ce urmează. Simptomul nu este o excepție; este un registru de lucru care se citește cu gunoi plauzibil în el

Înregistrările de formulă ocupă o numerotare paralelă în toate cele trei, $0006, $0206 și $0406. Când o formulă produce un rezultat de tip șir, acel șir sosește într-o înregistrare separată următoare, $0007 sau $0207, iar forma BIFF2 a acesteia folosește un prefix de lungime de un octet, nu cel de doi octeți folosit ulterior

De ce formulele revin ca valori, nu ca text

HotXLS citește rezultatul din cache al unei formule în aceste fișiere și nu încearcă să reconstruiască expresia formulei. Aceasta este o limită deliberată, nu un gol de umplut

Expresia analizată în BIFF2 până la BIFF4 folosește o codificare de tokenuri care diferă de BIFF5 și versiunile ulterioare în moduri ce depășesc cosmetica: lungimile tokenurilor au prefixe diferite, tokenurile de referință au dimensiuni diferite, iar tabelele de index de funcții au fost renumerotate între generații. Trecerea acelor octeți printr-un traducător de expresii BIFF8 nu produce o formulă greșită, produce una aleatorie. Citirea valorii din cache îți dă numărul sau șirul pe care Excel l-a calculat ultima dată, ceea ce este exact ce are nevoie o migrare de arhivă

Valoarea din cache locuiește la un offset dependent de generație în interiorul înregistrării: octetul 7 pentru BIFF2 și octetul 6 pentru BIFF3 și BIFF4. Valorile speciale, șiruri, boolean-uri, erori și celule blank, sunt codificate într-un cuvânt marker de $FFFF cu un discriminator, aceeași convenție pe care generațiile BIFF ulterioare au păstrat-o

Deschiderea unuia

Codul apelant nu are nimic remarcabil, și tocmai asta e ideea. Detecția se întâmplă în interiorul 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[] este bazat pe unu
    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;

Observă aritmetica de indici din acea buclă. Limitele UsedRange sunt bazate pe zero, în timp ce atât colecția de foi, cât și accesul la celule sunt bazate pe unu, o inconsecvență anterioară API-ului curent și păstrată pentru compatibilitate. Uitarea ajustării auditează dreptunghiul greșit și nu raportează nimic neobișnuit în timp ce face asta. Verificările prealabile ieftine care evită încărcarea unui fișier sunt acoperite în inspecția ușoară a registrului de lucru

Ce nu obții și ce să faci în privința asta

Formatarea nu este interpretată. HotXLS nu analizează înregistrările XF și FONT ale acestor generații, așa că fonturile, culorile, chenarele și formatele de numere nu sunt disponibile, iar celulele pe care Excel le-a afișat cândva ca date revin ca numerele lor seriale brute

Acel ultim aspect trebuie tratat în codul tău propriu, nu în cititor, iar motivul este onest: formatele de numere în BIFF2 până la BIFF4 nu sunt suficient de fiabile pentru a conduce o decizie automată de dată. O coloană de numere din cinci cifre ar putea fi date, sau ar putea fi coduri de piese. Convertește deliberat, folosind sistemul de dată al registrului de lucru, ale cărui reguli sunt descrise în numerele seriale de dată, sistemul 1904 și formatele de numere:

// Decide per coloană, niciodată per valoare: un număr din cinci cifre poate fi o
// dată sau un cod de piesă, iar formatul vechi nu îți va spune
if ColumnHoldsDates(C) then
begin
  // Cele două sisteme de dată sunt la 1462 de zile distanță, așa că același
  // număr serial denotă două date la patru ani distanță. Citește sistemul
  // din registrul de lucru, în loc să presupui unul
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

Două note structurale completează imaginea. Protecția prin parolă și înregistrările de pagină de coduri apar în interiorul fluxului singurei foi de lucru, nu într-un flux la nivel de registru de lucru, deoarece nu există niciun flux la nivel de registru de lucru unde să fie puse, așa că trebuie recunoscute în context de foaie de lucru. Iar un fișier BIFF2 până la BIFF4 conține exact un substream de foaie; registrele de lucru cu mai multe foi nu au existat până când formatul nu a câștigat containerul său

Calea pragmatică de migrare este așadar în doi pași: citește fișierul vechi pentru valorile lui, apoi scrie un registru de lucru modern care poartă acele valori cu formatarea pe care o aplici tu însuți. Citirea vechilor formate, scrierea modernă și tot ce e între ele rulează într-o singură bibliotecă pentru Delphi și C++Builder, descrisă pe pagina componentei de foi de calcul HotXLS pentru Delphi