Artigo Técnico

Ler Ficheiros Excel 2.0 a 4.0 em Delphi com o HotXLS

O HotXLS abre pastas de trabalho escritas pelo Excel 2.0, 3.0 e 4.0 diretamente a partir do Delphi e do C++Builder. Estes ficheiros são anteriores ao contentor de documento composto OLE que todos os .xls posteriores usam, pelo que são fluxos de registos BIFF em bruto, sem qualquer invólucro de armazenamento, e um leitor construído para BIFF8 não encontra lá dentro uma única estrutura reconhecível. Abrir um destes ficheiros usa a mesma chamada Open que qualquer outra pasta de trabalho; o leitor deteta o formato e muda de percurso

Estes ficheiros continuam a aparecer, e é essa a única razão pela qual tudo isto importa. Arquivos de engenharia, retenção de registos governamentais, dados laboratoriais de instrumentos cujo software de controlo foi escrito em 1993, e sistemas de contabilidade de longa duração deixaram todos para trás pastas de trabalho BIFF2 e BIFF4. O Excel moderno recusa-se a abrir várias delas por completo, tendo removido conversores legados por razões de segurança, o que deixa um conjunto de dados que ninguém consegue ler com uma ferramenta que alguém tenha

O que torna diferente uma pasta de trabalho anterior ao OLE?

Todo o .xls a partir do Excel 5.0 é um ficheiro composto OLE2, um pequeno sistema de ficheiros dentro de um ficheiro, com a pasta de trabalho a residir num fluxo chamado Workbook ou Book. Analisar um destes ficheiros começa por analisar esse contentor, conforme descrito em o formato binário de ficheiro composto em Pascal

Do BIFF2 ao BIFF4 não existe contentor. O ficheiro começa imediatamente com um registo BOF, e o número de registo desse BOF codifica a geração: $0009 para BIFF2, $0209 para BIFF3 e $0409 para BIFF4. O HotXLS valida o comprimento do corpo do BOF, entre quatro e seis bytes, e o tipo de subfluxo, $0010 para uma folha de cálculo, $0020 para um gráfico e $0040 para uma folha de macro, antes de se comprometer com o percurso em bruto. É essa validação que impede que um ficheiro corrompido ou mal identificado seja interpretado como uma pasta de trabalho muito antiga

Três gerações, três esquemas de registo

Os registos de célula são onde as gerações divergem de forma mais visível. O BIFF2 ocupa um bloco contíguo de números de registo baixos, de $0001 a $0005 para células em branco, inteiras, numéricas, de rótulo e booleanas ou de erro, e cada corpo transporta um campo de atributo de três bytes onde as versões posteriores colocam um índice de formato estendido. O BIFF3 e o BIFF4 abandonam isso e reutilizam os números de registo e os esquemas do BIFF5, $0201, $0203, $0204 e $0205, com um índice XF de dois bytes

Esse último detalhe causa uma falha específica e facilmente mal diagnosticada. Um registo LABEL de BIFF3 ou BIFF4 é estruturalmente idêntico ao seu homólogo de BIFF5, linha e coluna seguidas do índice de formato e depois a contagem de carateres. Escreva um leitor que assuma o esquema do BIFF2 e ele lê dois bytes a menos, depois sai fora do fim do registo e interpreta mal tudo o que vem a seguir. O sintoma não é uma exceção; é uma pasta de trabalho que se lê com lixo plausível lá dentro

Os registos de fórmula ocupam uma numeração paralela nas três gerações, $0006, $0206 e $0406. Quando uma fórmula produz um resultado em string, essa string chega num registo seguinte separado, $0007 ou $0207, e a forma BIFF2 desse registo usa um prefixo de comprimento de um único byte em vez do prefixo de dois bytes usado mais tarde

Por que razão as fórmulas voltam como valores, não como texto

O HotXLS lê o resultado em cache de uma fórmula nestes ficheiros e não tenta reconstruir a expressão da fórmula. Este é um limite deliberado, não uma lacuna à espera de ser preenchida

A expressão analisada de BIFF2 a BIFF4 usa uma codificação de tokens que difere do BIFF5 e posteriores de formas que vão além do cosmético: os comprimentos de token têm prefixos diferentes, os tokens de referência têm tamanhos diferentes, e as tabelas de índice de funções foram renumeradas entre gerações. Passar esses bytes por um tradutor de expressões BIFF8 não produz uma fórmula errada, produz uma fórmula aleatória. Ler o valor em cache dá o número ou a string que o Excel calculou pela última vez, que é aquilo de que uma migração de arquivo realmente precisa

O valor em cache reside num deslocamento dependente da geração dentro do registo: byte 7 para BIFF2 e byte 6 para BIFF3 e BIFF4. Os valores especiais, strings, booleanos, erros e células em branco, são codificados numa palavra marcadora de $FFFF com um discriminador, a mesma convenção que as gerações BIFF posteriores mantiveram

Abrir um destes ficheiros

O código de chamada não tem nada de especial, e é essa a questão. A deteção acontece dentro de 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[] é indexado a partir de 1
    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;

Repare na aritmética de índices desse ciclo. Os limites de UsedRange são indexados a partir de zero, enquanto tanto a coleção de folhas como o acesso a células são indexados a partir de um, uma inconsistência anterior à API atual e mantida por compatibilidade. Esquecer o ajuste audita o retângulo errado e não reporta nada de anormal enquanto o faz. As verificações prévias económicas que evitam carregar o ficheiro por completo estão descritas em inspeção leve de pastas de trabalho

O que não se obtém, e o que fazer quanto a isso

A formatação não é interpretada. O HotXLS não analisa os registos XF e FONT destas gerações, pelo que os tipos de letra, as cores, os limites e os formatos de número não estão disponíveis, e as células que o Excel outrora apresentava como datas voltam como os seus números de série em bruto

Esse último ponto precisa de ser tratado no código do próprio utilizador, e não no leitor, e a razão é honesta: os formatos de número em BIFF2 a BIFF4 não são suficientemente fiáveis para orientar uma decisão automática de data. Uma coluna de números de cinco dígitos pode ser datas, ou pode ser números de peça. Converta deliberadamente, usando o sistema de datas da pasta de trabalho, cujas regras estão descritas em números de série de data, o sistema de 1904 e formatos de número:

// Decida por coluna, nunca por valor: um número de cinco dígitos pode
// ser uma data ou um número de peça, e o formato legado não o dirá
if ColumnHoldsDates(C) then
begin
  // Os dois sistemas de datas distam 1462 dias, pelo que o mesmo
  // número de série denota duas datas com quatro anos de diferença.
  // Leia o sistema a partir da pasta de trabalho em vez de o assumir
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

Duas notas estruturais completam o quadro. Os registos de proteção por palavra-passe e de página de código aparecem dentro do único fluxo de folha de cálculo em vez de num fluxo ao nível da pasta de trabalho, porque não existe nenhum fluxo ao nível da pasta de trabalho onde os colocar, pelo que têm de ser reconhecidos em contexto de folha de cálculo. E um ficheiro BIFF2 a BIFF4 contém exatamente um subfluxo de folha; as pastas de trabalho com várias folhas não existiam até o formato ganhar o seu contentor

O percurso de migração pragmático é, portanto, um percurso em dois passos: ler o ficheiro legado pelos seus valores, depois escrever uma pasta de trabalho moderna que transporte esses valores com formatação aplicada pelo próprio utilizador. A leitura de ficheiros legados, a escrita moderna e tudo o que fica entre as duas correm numa única biblioteca para Delphi e C++Builder, descrita na página do componente de folha de cálculo para Delphi HotXLS