Artículo técnico

Leer archivos de Excel 2.0 a 4.0 en Delphi con HotXLS

HotXLS abre libros de trabajo escritos por Excel 2.0, 3.0 y 4.0 directamente desde Delphi y C++Builder. Estos archivos son anteriores al contenedor de documento compuesto OLE que usa cualquier .xls posterior, así que son flujos de registros BIFF sin procesar y sin ningún envoltorio de almacenamiento, y un lector construido para BIFF8 no encontrará dentro de ellos ni una sola estructura reconocible. Abrir uno usa la misma llamada Open que cualquier otro libro de trabajo; el lector detecta el formato y cambia de ruta

Estos archivos todavía aparecen, que es la única razón por la que todo esto importa. Archivos de ingeniería, retención de registros gubernamentales, datos de laboratorio de instrumentos cuyo software de control se escribió en 1993, y sistemas de contabilidad de larga data, todos dejaron atrás libros de trabajo BIFF2 y BIFF4. El Excel moderno se niega directamente a abrir varios de ellos, ya que eliminó los conversores heredados por razones de seguridad, lo que deja un conjunto de datos que nadie puede leer con una herramienta que alguien tenga

¿Qué hace distinto a un libro de trabajo previo a OLE?

Todo .xls desde Excel 5.0 en adelante es un archivo compuesto OLE2, un pequeño sistema de archivos dentro de un archivo, con el libro de trabajo residiendo en un flujo llamado Workbook o Book. Analizar uno empieza por analizar ese contenedor, como se describe en el formato binario de archivo compuesto en Pascal

De BIFF2 a BIFF4 no hay contenedor. El archivo comienza de inmediato con un registro BOF, y el número de registro de ese BOF codifica la generación: $0009 para BIFF2, $0209 para BIFF3 y $0409 para BIFF4. HotXLS valida la longitud del cuerpo del BOF, que está entre cuatro y seis bytes, y el tipo de subflujo, $0010 para una hoja de cálculo, $0020 para un gráfico y $0040 para una hoja de macros, antes de comprometerse con la ruta sin procesar. Esa validación es lo que evita que un archivo corrupto o mal identificado se interprete como un libro de trabajo muy antiguo

Tres generaciones, tres disposiciones de registro

Los registros de celda son donde las generaciones divergen de forma más visible. BIFF2 ocupa un bloque contiguo de números de registro bajos, de $0001 a $0005 para celdas en blanco, entero, número, etiqueta y booleano o error, y cada cuerpo lleva un campo de atributo de tres bytes donde las versiones posteriores colocan un índice de formato extendido. BIFF3 y BIFF4 abandonan eso y reutilizan los números de registro y las disposiciones de BIFF5, $0201, $0203, $0204 y $0205, con un índice XF de dos bytes

Ese último detalle provoca una falla específica y fácil de diagnosticar mal. Un registro LABEL de BIFF3 o BIFF4 es estructuralmente idéntico a su contraparte de BIFF5: fila y columna seguidas del índice de formato y luego el conteo de caracteres. Escribe un lector que asuma la disposición de BIFF2 y leerá dos bytes de menos, luego se saldrá del final del registro y malinterpretará todo lo que viene después. El síntoma no es una excepción; es un libro de trabajo que se lee con basura verosímil dentro

Los registros de fórmula ocupan una numeración paralela en las tres generaciones, $0006, $0206 y $0406. Cuando una fórmula produce un resultado de tipo cadena, esa cadena llega en un registro siguiente separado, $0007 o $0207, y la forma BIFF2 de esto usa un prefijo de longitud de un solo byte en lugar del de dos bytes usado después

Por qué las fórmulas regresan como valores, no como texto

HotXLS lee el resultado en caché de una fórmula en estos archivos y no intenta reconstruir la expresión de la fórmula. Este es un límite deliberado, no un vacío pendiente de llenar

La expresión analizada en BIFF2 a BIFF4 usa una codificación de tokens que difiere de BIFF5 y versiones posteriores de formas que van más allá de lo cosmético: las longitudes de token llevan un prefijo distinto, los tokens de referencia tienen tamaños distintos, y las tablas de índice de funciones se renumeraron entre generaciones. Pasar esos bytes por un traductor de expresiones de BIFF8 no produce una fórmula equivocada, produce una aleatoria. Leer el valor en caché da el número o la cadena que Excel calculó por última vez, que es lo que una migración de archivo realmente necesita

El valor en caché reside en un desplazamiento dependiente de la generación dentro del registro: byte 7 para BIFF2 y byte 6 para BIFF3 y BIFF4. Los valores especiales, cadenas, booleanos, errores y celdas en blanco, se codifican en una palabra marcadora de $FFFF con un discriminador, la misma convención que conservaron las generaciones BIFF posteriores

Abrir uno

El código que llama no tiene nada de particular, y esa es la idea. La detección ocurre 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[] usa base uno
    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;

Observa la aritmética de índices en ese bucle. Los límites de UsedRange usan base cero mientras que tanto la colección de hojas como el acceso a celdas usan base uno, una inconsistencia anterior a la API actual y que se conserva por compatibilidad. Olvidar el ajuste audita el rectángulo equivocado y no reporta nada fuera de lo común mientras lo hace. Las verificaciones previas económicas que evitan cargar un archivo por completo se cubren en inspección ligera de libros de trabajo

Qué no se obtiene, y qué hacer al respecto

El formato no se interpreta. HotXLS no analiza los registros XF y FONT de estas generaciones, así que las fuentes, los colores, los bordes y los formatos de número no están disponibles, y las celdas que Excel alguna vez mostró como fechas regresan como sus números de serie sin procesar

Ese último punto necesita manejarse en tu propio código y no en el lector, y la razón es honesta: los formatos de número en BIFF2 a BIFF4 no son lo bastante confiables como para impulsar una decisión automática de fecha. Una columna de números de cinco dígitos podría ser fechas, o podría ser números de pieza. Convierte de forma deliberada, usando el sistema de fechas del libro de trabajo, cuyas reglas se describen en números de serie de fecha, el sistema 1904 y los formatos de número:

// Decide por columna, nunca por valor: un número de cinco dígitos
// puede ser una fecha o un número de pieza, y el formato heredado no lo dirá
if ColumnHoldsDates(C) then
begin
  // Los dos sistemas de fecha están separados por 1462 días, así que
  // el mismo número de serie denota dos fechas a cuatro años de distancia.
  // Lee el sistema desde el libro de trabajo en lugar de asumir uno
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

Dos notas estructurales completan el panorama. Los registros de protección por contraseña y de página de códigos aparecen dentro del único flujo de hoja de cálculo en lugar de en un flujo a nivel de libro de trabajo, porque no existe ningún flujo a nivel de libro de trabajo donde colocarlos, así que hay que reconocerlos en contexto de hoja de cálculo. Y un archivo BIFF2 a BIFF4 contiene exactamente un subflujo de hoja; los libros de trabajo con varias hojas no existieron hasta que el formato obtuvo su contenedor

La ruta de migración pragmática es, por lo tanto, de dos pasos: leer el archivo heredado por sus valores, y luego escribir un libro de trabajo moderno que lleve esos valores con un formato que se aplique por cuenta propia. La lectura de archivos heredados, la escritura moderna y todo lo que hay entre medio se ejecutan en una sola biblioteca para Delphi y C++Builder, descrita en la página del componente de hoja de cálculo para Delphi HotXLS