Artykuł techniczny

Odczyt plików Excel 2.0-4.0 w Delphi za pomocą HotXLS

HotXLS otwiera bezpośrednio z Delphi i C++Builder skoroszyty zapisane przez Excel 2.0, 3.0 i 4.0. Te pliki poprzedzają kontener złożonego dokumentu OLE, którego używa każdy późniejszy plik .xls, więc są surowymi strumieniami rekordów BIFF bez jakiejkolwiek otoczki magazynu, a czytnik zbudowany pod BIFF8 nie znajdzie w nich ani jednej rozpoznawalnej struktury. Otwarcie takiego pliku korzysta z tego samego wywołania Open co każdy inny skoroszyt; czytnik wykrywa format i przełącza ścieżki

Te pliki wciąż się pojawiają, i to jedyny powód, dla którego cokolwiek z tego ma znaczenie. Archiwa inżynieryjne, przechowywanie rejestrów rządowych, dane laboratoryjne z przyrządów, których oprogramowanie sterujące napisano w 1993 roku, oraz długo działające systemy księgowe pozostawiły po sobie skoroszyty BIFF2 i BIFF4. Współczesny Excel wprost odmawia otwarcia wielu z nich, ponieważ usunął konwertery starszych formatów ze względów bezpieczeństwa, co pozostawia zbiór danych, którego nikt nie potrafi odczytać żadnym powszechnie dostępnym narzędziem

Co odróżnia skoroszyt przed-OLE-owy?

Każdy plik .xls od Excela 5.0 wzwyż to złożony plik OLE2, mały system plików wewnątrz pliku, w którym skoroszyt mieszka w strumieniu o nazwie Workbook albo Book. Parsowanie takiego pliku zaczyna się od parsowania tego kontenera, jak opisano w binarnym formacie pliku złożonego w Pascalu

BIFF2 aż do BIFF4 nie mają kontenera. Plik zaczyna się od razu rekordem BOF, a numer rekordu tego BOF koduje generację: $0009 dla BIFF2, $0209 dla BIFF3 i $0409 dla BIFF4. HotXLS waliduje długość ciała BOF, która wynosi od czterech do sześciu bajtów, oraz typ podstrumienia — $0010 dla arkusza, $0020 dla wykresu i $0040 dla arkusza makr — zanim zaangażuje się w surową ścieżkę. To właśnie ta walidacja zapobiega interpretowaniu uszkodzonego albo błędnie zidentyfikowanego pliku jako bardzo starego skoroszytu

Trzy generacje, trzy układy rekordów

To rekordy komórek są miejscem, w którym generacje różnią się najbardziej widocznie. BIFF2 zajmuje spójny blok niskich numerów rekordów, od $0001 do $0005, dla komórek pustych, całkowitoliczbowych, liczbowych, etykiet i logiczno-błędnych, a każde ciało niesie trzybajtowe pole atrybutu tam, gdzie późniejsze wersje umieszczają rozszerzony indeks formatu. BIFF3 i BIFF4 z tego rezygnują i ponownie wykorzystują numery i układy rekordów BIFF5: $0201, $0203, $0204 i $0205, z dwubajtowym indeksem XF

Ten ostatni szczegół powoduje konkretną i łatwą do błędnego zdiagnozowania usterkę. Rekord LABEL w BIFF3 albo BIFF4 jest strukturalnie identyczny ze swoim odpowiednikiem z BIFF5: wiersz i kolumna, po nich indeks formatu, a następnie liczba znaków. Napisz czytnik zakładający układ BIFF2, a odczyta o dwa bajty za mało, po czym zejdzie poza koniec rekordu i błędnie zinterpretuje wszystko, co następuje. Objawem nie jest wyjątek; jest nim skoroszyt, który wczytuje się z wiarygodnie wyglądającymi śmieciami w środku

Rekordy formuł zajmują równoległe numerowanie we wszystkich trzech generacjach: $0006, $0206 i $0406. Gdy formuła daje wynik tekstowy, ten tekst przychodzi w osobnym, następującym po nim rekordzie, $0007 albo $0207, a jego postać BIFF2 używa jednobajtowego prefiksu długości zamiast dwubajtowego, używanego później

Dlaczego formuły wracają jako wartości, a nie jako tekst

HotXLS odczytuje w tych plikach zbuforowany wynik formuły i nie próbuje odtworzyć wyrażenia formuły. To celowa granica, a nie luka czekająca na wypełnienie

Sparsowane wyrażenie w BIFF2 do BIFF4 używa kodowania tokenów, które różni się od BIFF5 i późniejszych w sposób wykraczający poza kosmetykę: długości tokenów są prefiksowane inaczej, tokeny odwołań mają inne rozmiary, a tabele indeksów funkcji zostały przenumerowane między generacjami. Przepuszczenie tych bajtów przez translator wyrażeń BIFF8 nie daje błędnej formuły — daje losową. Odczyt zbuforowanej wartości daje liczbę albo tekst, jaki Excel ostatnio obliczył, a to właśnie jest to, czego naprawdę potrzebuje migracja archiwum

Zbuforowana wartość mieszka pod przesunięciem zależnym od generacji wewnątrz rekordu: bajt 7 dla BIFF2 i bajt 6 dla BIFF3 i BIFF4. Wartości specjalne — ciągi znaków, wartości logiczne, błędy i puste pola — są kodowane w słowie znacznika $FFFF z dyskryminatorem, tą samą konwencją, którą zachowały późniejsze generacje BIFF

Otwieranie pliku

Kod wywołujący jest niczym niewyróżniający się i o to właśnie chodzi. Wykrywanie odbywa się wewnątrz 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[] jest liczone od jedynki
    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;

Zwróć uwagę na arytmetykę indeksów w tej pętli. Granice UsedRange są liczone od zera, podczas gdy zarówno kolekcja arkuszy, jak i dostęp do komórek są liczone od jedynki — niespójność sprzed obecnego API, zachowana dla kompatybilności. Zapomnienie o tej korekcie audytuje niewłaściwy prostokąt i nie zgłasza przy tym niczego niezwykłego. Tanie wstępne sprawdzenia, które w ogóle unikają wczytywania pliku, są opisane w lekkiej inspekcji skoroszytu

Czego nie dostajesz i co z tym zrobić

Formatowanie nie jest interpretowane. HotXLS nie parsuje rekordów XF i FONT tych generacji, więc fonty, kolory, obramowania i formaty liczb są niedostępne, a komórki, które Excel niegdyś wyświetlał jako daty, wracają jako swoje surowe liczby seryjne

Tę ostatnią kwestię trzeba obsłużyć we własnym kodzie, a nie w czytniku, a powód jest uczciwy: formaty liczb w BIFF2 do BIFF4 nie są wystarczająco wiarygodne, by sterować automatyczną decyzją o dacie. Kolumna pięciocyfrowych liczb może być datami albo numerami części. Konwertuj świadomie, korzystając z systemu dat skoroszytu, którego reguły opisano w liczbach seryjnych dat, systemie 1904 i formatach liczb:

// Decyduj dla całej kolumny, nigdy dla pojedynczej wartości: pięciocyfrowa liczba może być
// datą albo numerem części, a stary format tego nie powie
if ColumnHoldsDates(C) then
begin
  // Oba systemy dat dzieli 1462 dni, więc ta sama liczba seryjna
  // oznacza dwie daty odległe o cztery lata. Odczytaj system ze
  // skoroszytu, zamiast go zakładać
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

Dwie uwagi strukturalne dopełniają obraz. Rekordy ochrony hasłem i strony kodowej pojawiają się wewnątrz pojedynczego strumienia arkusza, a nie w strumieniu na poziomie skoroszytu, ponieważ nie ma strumienia na poziomie skoroszytu, w którym można by je umieścić, więc trzeba je rozpoznawać w kontekście arkusza. A plik BIFF2 do BIFF4 zawiera dokładnie jeden podstrumień arkusza; skoroszyty wielarkuszowe nie istniały, dopóki format nie zyskał swojego kontenera

Pragmatyczna ścieżka migracji jest więc dwuetapowa: odczytaj stary plik dla jego wartości, a następnie zapisz nowoczesny skoroszyt, który niesie te wartości z formatowaniem zastosowanym samodzielnie. Odczyt starych formatów, nowoczesny zapis i wszystko pomiędzy działa w jednej bibliotece dla Delphi i C++Builder, opisanej na stronie komponentu arkusza kalkulacyjnego HotXLS dla Delphi