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