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
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 ouvertexrcsBeforeFirst— ouvert ou re-ciblé, aucune ligne lue encorexrcsActive— debout sur une ligne validexrcsEof— la feuille a été consommée jusqu’à la finxrcsCancelled— l’appelant a arrêté la passe délibérémentxrcsFaulted— 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
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
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