Article technique

Curseur de lignes en tirage : XLS, XLSX, ODS, CSV en Delphi

HotXLS lit des sources .xls, .xlsx, .xlsm, .ods, CSV et TSV à travers un unique curseur de lignes en tirage, TXLSRowCursor, dont FindFirst et FindNext avancent d’une ligne logique à la fois tandis que cette seule ligne reste en mémoire. Une machine à états à six valeurs sépare l’état avant première ligne de la fin de fichier, de l’annulation et de l’échec, et l’ancien lecteur à rappels est maintenant un adaptateur par-dessus le même curseur

Le scénario est familier à quiconque a livré une fonction d’import. Un .xlsx de 200 Mo arrive, vous câblez un gestionnaire OnCell, et la première exigence après « lis-le » est « arrête-toi après les cent premières écritures inversées ». Désormais la forme de votre code vous combat : la boucle vit à l’intérieur de la bibliothèque, votre gestionnaire doit lever un drapeau, chaque rappel suivant se déclenche quand même jusqu’à ce que l’analyseur s’en aperçoive, et l’état accumulé — combien de correspondances jusqu’ici, quelle colonne a apparié, quoi faire ensuite — doit vivre dans des champs d’une classe qui n’existe que pour donner au rappel un endroit où se poser. Rien de tout cela n’est un problème d’analyse. C’est un problème de flot de contrôle, et c’est celui qu’un curseur en tirage supprime

Ce que coûte réellement un rappel en poussée à 200 Mo

La poussée inverse le contrôle, et l’inversion est exactement ce qu’un appelant qui filtre ou qui joint ne peut pas se permettre. Avec une API à rappels la bibliothèque possède la boucle, si bien que l’appelant ne peut pas utiliser Break, ne peut pas entrelacer deux sources, ne peut pas confier le lecteur à une routine qui s’attend à être pilotée, et ne peut pas exprimer « jette un œil à la ligne suivante avant de décider » sans mise en mémoire tampon. Le coût n’est pas le débit — un chemin à rappels SAX bien écrit coule très bien en flux — c’est que chaque consommateur non trivial fait pousser une petite machine à états à lui pour simuler la boucle qu’il n’avait pas le droit d’écrire. Multipliez cela par quatre formats de fichiers, chacun historiquement avec son propre point d’entrée de balayage, et les sémantiques de filtrage, de formule et d’erreur commencent à diverger entre eux, ce qui est précisément la divergence que HotXLS s’est proposé de fermer

Comment un curseur en tirage change-t-il votre code appelant ?

Il vous rend la boucle, et avec elle le flot de contrôle Pascal ordinaire. TXLSRowCursor.Open accepte un nom de fichier ou un TStream, détecte le format, charge les chaînes partagées et les métadonnées de style de date une seule fois, et sélectionne la feuille 1. SelectSheet (en base un) ou SelectSheetByName re-cible une autre feuille de calcul et remet le curseur à avant première ligne. FindFirst et FindNext se positionnent alors sur la prochaine ligne peuplée — les lignes sans cellule décodable sont sautées, si bien que RowIndex peut sauter — et la ligne courante est exposée comme CellCount, Cells[] et ValueByCol[], toutes en base un sur l’axe des colonnes. Sortir de la boucle est un Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // saute la bande d'en-tête
    Cursor.IncludeColumn(1);     // ne décode que ces deux colonnes
    Cursor.IncludeColumn(7);
    if not Cursor.Open('postings-200mb.xlsx') then
      Exit;
    if not Cursor.SelectSheetByName('Ledger') then
      Exit;

    Hits := 0;
    if Cursor.FindFirst then
      repeat
        if VarToStr(Cursor.ValueByCol[7]) = 'REVERSED' then
        begin
          Inc(Hits);
          if Hits = 100 then
            Break;               // Break ordinaire ; pas de drapeau d'abandon, pas de sentinelle
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // le destructeur termine la passe
  end;
end;

La projection et la plage se règlent avant la passe, pas filtrées après coup. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates et DetectTextTypes sont tous honorés à l’intérieur des moteurs, si bien qu’une colonne non sélectionnée n’alloue jamais sa valeur, sa chaîne de formule ou sa charge utile de texte enrichi en premier lieu — la suite de régression le prouve avec des formules de 16 KiB et des chaînes en cache qui ne sont jamais matérialisées quand leur colonne n’est pas projetée. Ces options sont délibérément gelées pendant qu’une passe est active et redeviennent inscriptibles à la fin de fichier, sur SelectSheet, ou après Close, pour qu’un balayage ne puisse jamais mélanger deux contrats de décodage. Si vous n’avez besoin que de l’inventaire des feuilles plutôt que des lignes, le chargement de métadonnées seules et de feuilles sélectives est le point d’entrée moins cher

Un moteur par format, une boucle de balayage chacun

Chaque format a exactement un balayeur avant à l’intérieur de HotXLS, et le curseur en tirage comme le lecteur à rappels pilotent ce même balayeur. TXLSXForwardRowBackend est l’unique machine à états SAX de feuille de calcul pour les parties feuille ECMA-376 Partie 1 §18.3, portant le lecteur XML, la table de formules partagées et l’analyseur de texte enrichi, et il avance vers exactement une frontière physique de <row> par appel. TXLSBiffForwardParser possède les globales, la sélection de feuille et l’avance de ligne pour le flux d’enregistrements [MS-XLS] ; le rendre pausable a produit la contrainte la plus tranchante de toute la conception, parce qu’une formule de chaîne en cache est un enregistrement Formula immédiatement suivi d’un enregistrement String, si bien qu’un point de suspension par ligne ne doit jamais atterrir entre les deux. TXLSForwardTextBackend garde un lecteur conscient du BOM, le délimiteur actif et un enregistrement logique — le CSV renifle virgule, point-virgule, tabulation ou barre verticale à partir du premier enregistrement tout en ignorant les caractères entre guillemets, et les champs entre guillemets multilignes sont joints avec #10 pour que le numéro de ligne suive les enregistrements logiques plutôt que les nouvelles lignes physiques. TXLSForwardOdsBackend garde un unique modèle de ligne physique pour les tables OpenDocument §9, traite table:number-rows-repeated comme un compte restant plutôt que comme une expansion, et avance au-delà des cellules couvertes sans émettre de valeurs. Le lecteur direct en flux partage le même chargeur de chaînes partagées et de styles de date

Le curseur de lignes en tirage HotXLS se répartissant vers un balayeur avant par format, un moteur SAX pour XLSX, un analyseur d’enregistrements pour BIFF, un moteur texte à délimiteur reniflé et un modèle de ligne ODS, avec le lecteur à rappels configuré par-dessus comme un adaptateur
Chaque format a exactement un balayeur avant, et le curseur en tirage comme le lecteur à rappels pilotent ce même balayeur, si bien que les sémantiques de filtrage et d’erreur ne peuvent pas diverger

Pourquoi six états plutôt qu’un drapeau Eof ?

Parce qu’un booléen unique rend quatre situations différentes indiscernables, et les appelants devinent faux sur toutes. TXLSRowCursorState les nomme explicitement

  • xrcsClosed — aucune source n’est ouverte
  • xrcsBeforeFirst — ouvert ou re-ciblé, aucune ligne lue encore
  • xrcsActive — debout sur une ligne valide
  • xrcsEof — la feuille a été consommée jusqu’à la fin
  • xrcsCancelled — l’appelant a arrêté la passe délibérément
  • xrcsFaulted — la passe a échoué et l’exception d’origine a été levée

Cette dernière distinction est celle qui compte en production. Une partie feuille manquante ou un démarrage de passe raté garde son EReadError et déplace le curseur vers xrcsFaulted ; il n’est jamais rétrogradé en un simple False qu’un appelant lirait comme « cette feuille était vide ». Cancel est délibérément plus étroit que Close : il ferme le moteur de feuille courant et son sous-flux de décompression et invalide la ligne courante, mais il ne libère ni l’archive ZIP ni le flux source, et l’appeler deux fois est un no-op. Après une annulation vous reprenez en appelant SelectSheet explicitement — le curseur ne redémarrera pas tranquillement une passe à votre place. La propriété des flux suit la même règle défensive : xsoBorrowed est le défaut et restaure la position du flux à la fermeture, xsoOwned transfère la propriété seulement après que Open a déjà réussi, si bien qu’une ouverture ratée ne libère jamais un flux que l’appelant détient encore

Les six états du curseur de lignes HotXLS avec les transitions entre eux, montrant Cancel déplaçant une passe active vers annulé, un démarrage de passe raté le déplaçant vers en échec, et comment tous deux restent distincts de la fin de feuille
Six états nommés gardent une feuille vide, un arrêt délibéré et une passe ratée discernables, ce qu’un booléen Eof unique ne peut pas faire
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed : le curseur ne libère jamais Src, et Close restaure la
      // position qu'avait le flux quand Open a été appelé
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // ferme le moteur de feuille et son
            Break;           // sous-flux de décompression seulement ; idempotent
          end;
        until not Cursor.FindNext;

      case Cursor.State of
        xrcsEof:       Log('sheet consumed to the end');
        xrcsCancelled: Log('stopped by the operator');
        xrcsFaulted:   Log('pass failed; the EReadError was already raised');
      end;
    finally
      Cursor.Free;
    end;
  finally
    Src.Free;                // toujours à nous, toujours valide, position restaurée
  end;
end;

Emprunter la ligne courante sans la copier

IXLSRowCursorView confie une ligne à une autre routine sans dupliquer le tableau de cellules. La vue enregistre un garde partagé portant le pointeur de curseur plus un compteur de génération UInt64 ; avancer, sélectionner une feuille, annuler, fermer et détruire le curseur incrémentent tous cette génération, et la destruction efface de surcroît le propriétaire du garde. Ainsi une vue périmée ne peut pas lire de la mémoire libérée : Valid est une sonde sans exception que vous pouvez appeler à tout moment, tandis que chaque autre membre valide d’abord et lève EXLSRowCursorViewInvalidated. Soyez honnête sur ce qu’est ce contrat — c’est un échec rapide de durée de vie, pas une garantie de sûreté de threads, et il ne permet pas de lire une ligne depuis un second thread pendant que le premier avance le curseur

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // emprunte ; aucun tableau de cellules n'est copié
      for I := 0 to View.CellCount - 1 do
      begin
        Cell := View.Cells[I];
        if Cell.HasFormula and not Cell.FormulaTextAvailable then
          UseCachedResult(Cell.Value)     // les lectures avant BIFF gardent le
        else if Cell.Kind = xdkEmpty then //   résultat en cache, pas les jetons
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank sont de vraies cellules
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // L'interface survit à la boucle, mais la ligne derrière elle non
  if not View.Valid then    // Valid ne lève jamais ; Cells[] maintenant lèverait
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, et ce qu’il a le droit de prouver

PeakRowBufferedBytes existe pour démontrer que la mémoire suit la largeur de ligne plutôt que le nombre de lignes. Il accumule les enregistrements de cellules, les Variants, les chaînes de formule et les charges utiles de texte enrichi de la ligne de sortie courante et intègre l’ensemble de travail propre au format — l’enregistrement logique CSV, le modèle de ligne physique ODS, le pic d’enregistrements BIFF, ou la cellule brute XLSX en cours de décodage. Lisez-le avec SheetPassesStarted, qui compte combien de passes de feuille ont réellement commencé. Deux réserves gardent cela honnête : le chiffre est une estimation, pas une comptabilité exacte du tas, et il est monotone depuis le Open le plus récent, si bien que c’est un instrument de débogage et de régression plutôt qu’une jauge en direct. Pour la vue plus large de où vont le temps et les octets sur de très gros classeurs, voir la performance des gros classeurs en Delphi

Une comparaison HotXLS montrant un chargement de feuille entière gardant chaque ligne résidente face au curseur en tirage ne retenant que la ligne courante plus un ensemble de travail de format, ce que PeakRowBufferedBytes accumule et rapporte
PeakRowBufferedBytes accumule la ligne de sortie courante plus l’ensemble de travail propre au format, si bien que la mémoire suit la largeur d’une ligne plutôt que le nombre de lignes de la feuille

Le lecteur en poussée est devenu un adaptateur, et ce que le curseur ne fera pas

TXLSForwardReader ne porte plus de points d’entrée de balayage XLSX, BIFF et texte séparés. Il configure un curseur, le parcourt, et traduit la ligne courante en événements OnSheet et OnCell, c’est pourquoi les deux façades ne peuvent plus diverger sur le filtrage, l’état de formule ou la gestion d’erreurs. Deux conséquences valent la peine d’être connues avant de mettre à niveau : le SheetIndex des rappels est maintenant uniformément en base un sur TXLSForwardReader (TXLSDirectReader garde son contrat d’événement existant en base zéro), et OnSheet se déclenche avant SelectSheet, si bien que fixer SkipSheet signifie que la partie feuille n’est jamais ouverte ni décompressée du tout. Les frontières sont tout aussi explicites : le classeur ne doit pas être modifié pendant qu’une passe est active, l’annulation exige un redémarrage explicite, et le chemin avant BIFF ne décompile jamais les jetons de formule, si bien que les cellules à formule classiques signalent HasFormula vrai avec FormulaTextAvailable faux et vous remettent le résultat en cache au lieu d’inventer une chaîne de formule vide. Le curseur de lignes et son adaptateur ont passé 1 298 vérifications sur Delphi Win32 et Win64 plus le paquet statique C++Builder 37.0 Win64

Si vous pesez un curseur en tirage contre le chargeur que vous avez actuellement, la question à poser n’est pas lequel analyse plus vite mais lequel vous laisse écrire la condition de sortie dont vous avez réellement besoin. Les détails complets du composant, les versions d’IDE prises en charge et les licences sont sur la page du composant tableur HotXLS pour Delphi