Article technique

Lire les fichiers Excel 2.0 à 4.0 dans Delphi avec HotXLS

HotXLS ouvre les classeurs écrits par Excel 2.0, 3.0 et 4.0 directement depuis Delphi et C++Builder. Ces fichiers sont antérieurs au conteneur de document composé OLE qu'utilise chaque .xls ultérieur, ce sont donc des flux d'enregistrements BIFF bruts sans aucun emballage de stockage, et un lecteur conçu pour BIFF8 n'y trouvera pas la moindre structure reconnaissable. Ouvrir l'un d'eux utilise le même appel Open que n'importe quel autre classeur ; le lecteur détecte le format et change de chemin

Ces fichiers continuent d'apparaître, ce qui est la seule raison pour laquelle tout ceci compte. Les archives d'ingénierie, la conservation des dossiers publics, les données de laboratoire issues d'instruments dont le logiciel de contrôle a été écrit en 1993, et les systèmes comptables en service depuis longtemps ont tous laissé derrière eux des classeurs BIFF2 et BIFF4. L'Excel moderne refuse purement et simplement d'en ouvrir plusieurs, ayant retiré les convertisseurs hérités pour des raisons de sécurité, ce qui laisse un ensemble de données que plus personne ne peut lire avec un outil que quiconque possède

Qu'est-ce qui distingue un classeur antérieur à OLE ?

Chaque .xls à partir d'Excel 5.0 est un fichier composé OLE2, un petit système de fichiers à l'intérieur d'un fichier, le classeur résidant dans un flux nommé Workbook ou Book. Analyser un tel fichier commence par analyser ce conteneur, comme décrit dans le format binaire de fichier composé en Pascal

BIFF2 à BIFF4 n'ont aucun conteneur. Le fichier commence immédiatement par un enregistrement BOF, et le numéro d'enregistrement de ce BOF code la génération : $0009 pour BIFF2, $0209 pour BIFF3 et $0409 pour BIFF4. HotXLS valide la longueur du corps du BOF, comprise entre quatre et six octets, ainsi que le type de sous-flux, $0010 pour une feuille de calcul, $0020 pour un graphique et $0040 pour une feuille de macro, avant de s'engager sur le chemin brut. C'est cette validation qui empêche un fichier corrompu ou mal identifié d'être interprété comme un très ancien classeur

Trois générations, trois structures d'enregistrement

C'est au niveau des enregistrements de cellule que les générations divergent le plus visiblement. BIFF2 occupe un bloc contigu de numéros d'enregistrement bas, de $0001 à $0005 pour les cellules vides, entières, numériques, libellés et booléennes-ou-erreur, et chaque corps porte un champ d'attribut de trois octets là où les versions ultérieures placent un index de format étendu. BIFF3 et BIFF4 abandonnent cela et réutilisent les numéros d'enregistrement et structures de BIFF5, $0201, $0203, $0204 et $0205, avec un index XF de deux octets

Ce dernier détail provoque une défaillance spécifique et facile à mal diagnostiquer. Un enregistrement LABEL BIFF3 ou BIFF4 est structurellement identique à son équivalent BIFF5, ligne et colonne suivies de l'index de format puis du nombre de caractères. Écrivez un lecteur qui suppose la structure BIFF2 et il lira deux octets de trop peu, puis débordera de la fin de l'enregistrement et mal interprétera tout ce qui suit. Le symptôme n'est pas une exception ; c'est un classeur qui se lit avec des données absurdes mais plausibles à l'intérieur

Les enregistrements de formule occupent une numérotation parallèle sur les trois générations, $0006, $0206 et $0406. Lorsqu'une formule produit un résultat sous forme de chaîne, cette chaîne arrive dans un enregistrement suivant distinct, $0007 ou $0207, et la forme BIFF2 de celui-ci utilise un préfixe de longueur d'un octet plutôt que celui de deux octets utilisé plus tard

Pourquoi les formules reviennent comme des valeurs, pas comme du texte

HotXLS lit le résultat mis en cache d'une formule dans ces fichiers et ne tente pas de reconstruire l'expression de la formule. C'est une limite délibérée, pas une lacune en attente d'être comblée

L'expression analysée en BIFF2 à BIFF4 utilise un encodage de jetons qui diffère de BIFF5 et des versions ultérieures de manières qui vont au-delà du cosmétique : les longueurs de jetons sont préfixées différemment, les jetons de référence ont des tailles différentes, et les tables d'index de fonctions ont été renumérotées entre les générations. Faire passer ces octets par un traducteur d'expression BIFF8 ne produit pas une formule erronée, cela en produit une aléatoire. Lire la valeur mise en cache vous donne le nombre ou la chaîne qu'Excel a calculé en dernier, ce dont une migration d'archive a réellement besoin

La valeur mise en cache réside à un décalage dépendant de la génération à l'intérieur de l'enregistrement : octet 7 pour BIFF2 et octet 6 pour BIFF3 et BIFF4. Les valeurs spéciales, chaînes, booléens, erreurs et cellules vides, sont encodées dans un mot marqueur $FFFF avec un discriminateur, la même convention que les générations BIFF ultérieures ont conservée

En ouvrir un

Le code appelant n'a rien de particulier, et c'est bien le but. La détection se produit à l'intérieur d'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[] est indexé à un
    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;

Notez l'arithmétique d'index dans cette boucle. Les bornes d'UsedRange sont indexées à zéro alors que la collection de feuilles et l'accès aux cellules sont indexés à un, une incohérence antérieure à l'API actuelle et conservée par compatibilité. Oublier cet ajustement audite le mauvais rectangle et ne signale rien d'inhabituel en le faisant. Les vérifications préalables économiques qui évitent totalement de charger un fichier sont traitées dans l'inspection légère de classeurs

Ce que vous n'obtenez pas, et comment y remédier

La mise en forme n'est pas interprétée. HotXLS n'analyse pas les enregistrements XF et FONT de ces générations, donc les polices, couleurs, bordures et formats de nombre sont indisponibles, et les cellules qu'Excel affichait autrefois comme des dates reviennent sous forme de leurs numéros de série bruts

Ce dernier point doit être géré dans votre propre code plutôt que dans le lecteur, et la raison est honnête : les formats de nombre en BIFF2 à BIFF4 ne sont pas assez fiables pour piloter une décision automatique de date. Une colonne de nombres à cinq chiffres peut être des dates, ou peut être des numéros de pièce. Convertissez délibérément, en utilisant le système de dates du classeur, dont les règles sont décrites dans les numéros de série de date, le système 1904 et les formats de nombre :

// Décidez par colonne, jamais par valeur : un nombre à cinq chiffres peut être une
// date ou un numéro de pièce, et le format hérité ne vous le dira pas
if ColumnHoldsDates(C) then
begin
  // Les deux systèmes de dates sont décalés de 1462 jours, donc le même numéro
  // de série désigne deux dates espacées de quatre ans. Lisez le système depuis
  // le classeur plutôt que d'en supposer un
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

Deux remarques structurelles complètent le tableau. Les enregistrements de protection par mot de passe et de page de code apparaissent à l'intérieur du flux unique de la feuille de calcul plutôt que dans un flux au niveau du classeur, car il n'existe aucun flux au niveau du classeur où les placer, donc ils doivent être reconnus dans le contexte de la feuille de calcul. Et un fichier BIFF2 à BIFF4 contient exactement un sous-flux de feuille ; les classeurs multi-feuilles n'ont existé qu'à partir du moment où le format a gagné son conteneur

Le chemin de migration pragmatique se déroule donc en deux étapes : lire le fichier hérité pour ses valeurs, puis écrire un classeur moderne qui porte ces valeurs avec une mise en forme que vous appliquez vous-même. La lecture héritée, l'écriture moderne et tout ce qui se trouve entre les deux s'exécutent dans une seule bibliothèque pour Delphi et C++Builder, décrite sur la page du composant tableur Delphi HotXLS