HotXLS відкриває книги, записані Excel 2.0, 3.0 і 4.0, безпосередньо з Delphi та C++Builder. Ці файли передують складеному документ-контейнеру OLE, який використовує кожен пізніший .xls, тож вони являють собою необроблені потоки записів BIFF взагалі без жодної обгортки сховища, і читач, побудований для BIFF8, не знайде всередині них жодної впізнаваної структури. Відкриття такого файлу використовує той самий виклик Open, що й будь-яка інша книга; читач виявляє формат і перемикає шляхи обробки
Ці файли досі трапляються, і це єдина причина, чому все це має значення. Інженерні архіви, державне збереження документів, лабораторні дані з приладів, чиє керуюче програмне забезпечення написане 1993 року, і довготривалі бухгалтерські системи — усі вони лишили по собі книги BIFF2 і BIFF4. Сучасний Excel прямо відмовляється відкривати багато з них, видаливши застарілі конвертери з міркувань безпеки, що лишає набір даних, який ніхто не може прочитати жодним наявним інструментом
Чим доOLE-книга відрізняється?
Кожен .xls, починаючи з Excel 5.0, — це складений файл OLE2, маленька файлова система всередині файлу, де книга живе в потоці з ім'ям Workbook чи Book. Розбір такого файлу починається з розбору цього контейнера, як описано у статті двійковий формат складеного файлу в Pascal
BIFF2–BIFF4 не мають контейнера. Файл починається одразу з запису BOF, і номер запису цього BOF кодує покоління: $0009 для BIFF2, $0209 для BIFF3 і $0409 для BIFF4. HotXLS перевіряє довжину тіла BOF, яка становить від чотирьох до шести байтів, і тип підпотоку — $0010 для аркуша, $0020 для діаграми та $0040 для аркуша макросів — перш ніж переходити на необроблений шлях. Саме ця перевірка не дає пошкодженому чи неправильно ідентифікованому файлу бути витлумаченим як дуже стара книга
Три покоління, три компонування записів
Записи клітинок — це те місце, де покоління розходяться найпомітніше. BIFF2 займає суцільний блок низьких номерів записів, $0001 до $0005 для порожніх, цілочисельних, числових, текстових і логічних-або-помилкових клітинок, і кожне тіло несе трибайтне поле атрибутів, туди, куди пізніші версії кладуть індекс розширеного формату. BIFF3 і BIFF4 відмовляються від цього й повторно використовують номери записів і компонування BIFF5: $0201, $0203, $0204 і $0205, із двобайтним індексом XF
Ця остання деталь спричиняє специфічний і легко неправильно діагностований збій. Запис LABEL у BIFF3 чи BIFF4 структурно ідентичний своєму відповіднику в BIFF5: рядок і стовпець, за якими йдуть індекс формату, а потім кількість символів. Напишіть читач, що припускає компонування BIFF2, і він прочитає на два байти замало, потім вийде за межі запису й неправильно витлумачить усе, що йде далі. Симптом — не виняток; це книга, яка читається з правдоподібним сміттям усередині
Записи формул займають паралельну нумерацію в усіх трьох: $0006, $0206 і $0406. Коли формула дає рядковий результат, цей рядок надходить у окремому наступному записі, $0007 чи $0207, і форма BIFF2 цього запису використовує однобайтний префікс довжини замість двобайтного, що використовується пізніше
Чому формули повертаються як значення, а не як текст
HotXLS читає кешований результат формули в цих файлах і не намагається реконструювати вираз формули. Це свідома межа, а не прогалина, що чекає заповнення
Розібраний вираз у BIFF2–BIFF4 використовує кодування токенів, яке відрізняється від BIFF5 і пізніших способами, що виходять за межі косметики: довжини токенів префіксуються по-іншому, токени посилань мають інші розміри, а таблиці індексів функцій були перенумеровані між поколіннями. Пропускання цих байтів через транслятор виразів BIFF8 не дає неправильної формули — воно дає випадкову. Читання кешованого значення дає вам число чи рядок, які Excel обчислив востаннє, а це те, що насправді потрібно міграції архіву
Кешоване значення живе за зсувом усередині запису, залежним від покоління: байт 7 для BIFF2 і байт 6 для BIFF3 і BIFF4. Особливі значення — рядки, логічні значення, помилки й порожні клітинки — кодуються в маркерному слові $FFFF з дискримінатором, і саме цю угоду зберегли пізніші покоління BIFF
Відкриття файлу
Код виклику нічим не примітний, і в цьому й суть. Виявлення відбувається всередині 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[] з відліком від одиниці
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;
Зверніть увагу на арифметику індексів у цьому циклі. Межі UsedRange мають нульовий відлік, тоді як і колекція аркушів, і доступ до клітинок мають відлік від одиниці — невідповідність, що передує поточному API й зберігається заради сумісності. Забування цього коригування перевіряє не той прямокутник і при цьому не повідомляє нічого незвичного. Дешеві попередні перевірки, що взагалі уникають завантаження файлу, описано у статті легка перевірка книги
Чого ви не отримуєте і що з цим робити
Форматування не інтерпретується. HotXLS не розбирає записи XF і FONT цих поколінь, тож шрифти, кольори, рамки та числові формати недоступні, а клітинки, які Excel колись відображав як дати, повертаються як свої необроблені порядкові номери
Останнє потребує обробки у вашому власному коді, а не в читачі, і причина чесна: числові формати в BIFF2–BIFF4 недостатньо надійні, щоб керувати автоматичним рішенням про дату. Стовпець п'ятизначних чисел може бути датами, а може бути номерами деталей. Конвертуйте свідомо, використовуючи систему дат книги, чиї правила описано у статті порядкові номери дат, система 1904 та числові формати:
// Вирішуйте для кожного стовпця, ніколи для кожного значення:
// п'ятизначне число може бути датою або номером деталі, а
// застарілий формат вам цього не підкаже
if ColumnHoldsDates(C) then
begin
// Дві системи дат розходяться на 1462 дні, тож той самий
// порядковий номер позначає дві дати з різницею в чотири роки.
// Читайте систему з книги, а не припускайте якусь
if Book.Date1904 then
Writeln(DateToStr(SerialToDate1904(V)))
else
Writeln(DateToStr(SerialToDate1900(V)));
end
else
Writeln(VarToStr(V));
Дві структурні примітки завершують картину. Записи захисту паролем і кодової сторінки з'являються всередині єдиного потоку аркуша, а не в потоці рівня книги, бо немає потоку рівня книги, куди їх можна було б покласти, тож їх доводиться розпізнавати в контексті аркуша. А файл BIFF2–BIFF4 містить рівно один підпотік аркуша; багатоаркушеві книги не існували, доки формат не отримав свій контейнер
Тому прагматичний шлях міграції — двоетапний: прочитайте застарілий файл заради його значень, а потім запишіть сучасну книгу, що несе ці значення з форматуванням, яке ви застосовуєте самостійно. Застаріле читання, сучасний запис і все між ними працює в одній бібліотеці для Delphi та C++Builder, описаній на сторінці компонента електронних таблиць HotXLS для Delphi