Technisch artikel

Pull-rijcursor voor XLS, XLSX, ODS en CSV in Delphi

HotXLS leest .xls-, .xlsx-, .xlsm-, .ods-, CSV- en TSV-bronnen door één enkele pull-rijcursor, TXLSRowCursor, waarvan FindFirst en FindNext één logische rij per keer opschuiven terwijl alleen die rij in het geheugen blijft. Een statusmachine met zes waarden scheidt before-first van EOF, geannuleerd en gefaald, en de oudere callback-reader is nu een adapter over dezelfde cursor

Het scenario is bekend voor iedereen die ooit een importfunctie heeft uitgebracht. Er komt een .xlsx van 200 MB binnen, u sluit een OnCell-handler aan, en de eerste vereiste na "lees het" is "stop na de eerste honderd omgekeerde boekingen". Nu vecht de vorm van uw code tegen u: de lus leeft in de bibliotheek, uw handler moet een vlag zetten, elke volgende callback blijft afgaan totdat de parser het merkt, en de opgetelde status — hoeveel treffers tot nu toe, welke kolom matchte, wat hierna te doen — moet in velden op een klasse wonen die alleen bestaat om de callback ergens te laten zitten. Daar is niets dat een parseerprobleem is. Het is een controlflow-probleem, en het is precies datgene wat een pull-cursor wegneemt

Wat een push-callback feitelijk kost bij 200 MB

Push keert de controle om, en omkering is precies wat een filterende of samenvoegende aanroeper niet kan dragen. Met een callback-API bezit de bibliotheek de lus, dus de aanroeper kan geen Break gebruiken, kan geen twee bronnen interleaven, kan de reader niet aan een routine geven die verwacht gedreven te worden, en kan "eerst de volgende rij bekijken voordat wordt besloten" niet uitdrukken zonder bufferen. De kosten zitten niet in de doorvoer — een goed geschreven SAX-callbackpad streamt prima — ze zitten in het feit dat elke niet-triviale consument een eigen kleine statusmachine laat groeien om de lus te simuleren die haar niet werd toegestaan te schrijven. Vermenigvuldig dat met vier bestandsformaten, elk historisch met een eigen scan-instappunt, en de filter-, formule- en foutsemantiek beginnen uit elkaar te drijven, precies de drift die HotXLS wilde dichten

Hoe verandert een pull-cursor uw aanroepende code?

Ze geeft de lus aan u terug, en daarmee de gewone Pascal-controlflow. TXLSRowCursor.Open accepteert een bestandsnaam of een TStream, detecteert het formaat, laadt shared strings en datumstijl-metadata één keer, en selecteert werkblad 1. SelectSheet (1-based) of SelectSheetByName richt op een ander werkblad en reset de cursor naar before-first. FindFirst en FindNext positioneren daarna op de volgende gevulde rij — rijen zonder decodeerbare cellen worden overgeslagen, dus RowIndex kan springen — en de huidige rij wordt blootgesteld als CellCount, Cells[] en ValueByCol[], allemaal 1-based op de kolomas. De lus verlaten is een Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // sla de koptekstband over
    Cursor.IncludeColumn(1);     // decodeer alleen deze twee kolommen
    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;               // gewone Break; geen abort-vlag, geen sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // de destructor beëindigt de pass
  end;
end;

Projectie en bereik worden vóór de pass ingesteld, niet achteraf gefilterd. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates en DetectTextTypes worden allemaal in de backends geëerd, dus een niet-geselecteerde kolom wijst in de eerste plaats nooit haar waarde, formulestring of rich-text-payload toe — de regression suite bewijst dat met 16 KiB-formules en gecachte strings die nooit worden gematerialiseerd wanneer hun kolom niet is geprojecteerd. Die opties zijn bewust bevroren terwijl een pass actief is en worden weer beschrijfbaar bij EOF, bij SelectSheet, of na Close, dus één scan kan nooit twee decodeercontracten mengen. Als u alleen de werkbladinventaris nodig hebt in plaats van de rijen, is metadata-only en selectief werkblad laden het goedkopere instappunt

Eén backend per formaat, één scanlus elk

Elk formaat heeft precies één forward scanner in HotXLS, en zowel de pull-cursor als de callback-reader drijft diezelfde scanner aan. TXLSXForwardRowBackend is de enige werkblad-SAX-statusmachine voor ECMA-376 Part 1 §18.3 sheet-onderdelen, houdt de XML-reader, de shared-formula-tabel en de rich-text-parser vast, en schuift per aanroep precies één fysieke <row>-grens op. TXLSBiffForwardParser bezit de globals, de werkbladselectie en de rij-opschuiving voor de [MS-XLS]-recordstroom; die pauzeerbaar maken leverde de scherpste beperking van het hele ontwerp op, want een gecachte stringformule is een Formula-record onmiddellijk gevolgd door een String-record, dus een suspensiepunt per rij mag nooit tussen die twee landen. TXLSForwardTextBackend houdt een BOM-bewuste reader, de actieve delimiter en één logisch record vast — CSV ruikt komma, puntkomma, tab of pipe uit het eerste record terwijl quoted tekens worden genegeerd, en meerregelige quoted velden worden samengevoegd met #10 zodat het rijnummer logische records volgt in plaats van fysieke newlines. TXLSForwardOdsBackend houdt één fysiek rijsjabloon vast voor OpenDocument §9-tabellen, behandelt table:number-rows-repeated als een resterend-aantal in plaats van een expansie, en schuift voorbij gedekte cellen zonder waarden te emitten. De streaming direct reader deelt dezelfde shared-string- en datumstijl-loader

De HotXLS pull-rijcursor die doorstuurt naar één forward scanner per formaat: een SAX-backend voor XLSX, een recordparser voor BIFF, een delimiter-snuffelende tekstbackend en een ODS-rijsjabloon, met de callback-reader erbovenop geconfigureerd als adapter
Elk formaat heeft precies één forward scanner, en zowel de pull-cursor als de callback-reader drijft diezelfde scanner aan, dus filter- en foutsemantiek kunnen niet uit elkaar drijven

Waarom zes statussen in plaats van één Eof-vlag?

Omdat één boolean vier verschillende situaties ononderscheidbaar maakt, en aanroepers erover allemaal verkeerd gissen. TXLSRowCursorState benoemt ze expliciet

  • xrcsClosed — geen bron is open
  • xrcsBeforeFirst — geopend of opnieuw gericht, nog geen rij gelezen
  • xrcsActive — staand op een geldige rij
  • xrcsEof — het werkblad is tot het einde geconsumeerd
  • xrcsCancelled — de aanroeper heeft de pass bewust gestopt
  • xrcsFaulted — de pass is mislukt en de oorspronkelijke exception is gegooid

Dat laatste onderscheid is degene die er in productie toe doet. Een ontbrekend sheet-onderdeel of een mislukte pass-start behoudt zijn EReadError en verplaatst de cursor naar xrcsFaulted; het wordt nooit afgegradeerd naar een kale False die een aanroeper zou lezen als "dit werkblad was leeg". Cancel is bewust nauwer dan Close: die sluit de huidige werkblad-backend en diens inflate-substream en maakt de huidige rij ongeldig, maar geeft het ZIP-archief of de bronstroom niet vrij, en twee keer aanroepen is een no-op. Na een cancel hervat u door expliciet SelectSheet aan te roepen — de cursor herstart niet geruisloos een pass namens u. Stream-eigendom volgt dezelfde defensieve regel: xsoBorrowed is de standaard en herstelt de streampositie bij sluiten, xsoOwned draagt eigendom pas over nadat Open al is gelukt, dus een mislukte open geeft nooit een stroom vrij die de aanroeper nog vasthoudt

De zes statussen van de HotXLS-rijcursor met de overgangen ertussen, tonend hoe Cancel een actieve pass naar geannuleerd verplaatst, een mislukte pass-start naar gefaald, en hoe beide onderscheiden blijven van het einde van het werkblad
Zes benoemde statussen houden een leeg werkblad, een bewuste stop en een mislukte pass onderscheidbaar, wat één Eof-boolean niet kan
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: de cursor geeft Src nooit vrij, en Close herstelt de
      // positie die de stream had toen Open werd aangeroepen
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // sluit de werkblad-backend en diens
            Break;           // inflate-substream alleen; 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;                // nog steeds van ons, nog geldig, positie hersteld
  end;
end;

De huidige rij lenen zonder haar te kopiëren

IXLSRowCursorView geeft een rij aan een andere routine zonder de celarray te dupliceren. De view bewaart een gedeelde guard met de cursorpointer plus een UInt64-generatieteller; opschuiven, een werkblad selecteren, annuleren, sluiten en vernietigen van de cursor verhogen die generatie allemaal, en vernietiging wist bovendien de guard-eigenaar. Een verouderde view kan dus geen vrijgegeven geheugen lezen: Valid is een exceptionvrije probe die u op elk moment kunt aanroepen, terwijl elk ander lid eerst valideert en EXLSRowCursorViewInvalidated gooit. Wees eerlijk over wat dit contract is — het is lifetime fail-fast, geen thread-safety-garantie, en het geeft geen licentie om een rij te lezen vanuit een tweede thread terwijl de eerste de cursor opschuift

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // leent; er wordt geen celarray gekopieerd
      for I := 0 to View.CellCount - 1 do
      begin
        Cell := View.Cells[I];
        if Cell.HasFormula and not Cell.FormulaTextAvailable then
          UseCachedResult(Cell.Value)     // BIFF forward-lezingen houden de
        else if Cell.Kind = xdkEmpty then //   gecachte resultaten vast, niet de tokens
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank zijn echte cellen
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // De interface overleeft de lus, maar de rij erachter niet
  if not View.Valid then    // Valid gooit nooit; Cells[] zou nu gooien
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, en wat het mag bewijzen

PeakRowBufferedBytes bestaat om te laten zien dat geheugen de rijbreedte volgt in plaats van het rijantal. Het accumuleert de celrecords, Variants, formulestrings en rich-text-payloads van de huidige uitvoerrij en vouwt de formaatspecifieke working set erin — het CSV-logische record, het ODS-fysieke rijsjabloon, de BIFF-recordpiek, of de XLSX-raw cel die momenteel wordt gedecodeerd. Lees haar samen met SheetPassesStarted, dat telt hoeveel werkblad-passes feitelijk begonnen. Twee kanttekeningen houden dit eerlijk: het cijfer is een schatting, geen exacte heap-boekhouding, en hij is monotoon sinds de meest recente Open, dus het is een debugging- en regressie-instrument in plaats van een live meter. Voor het bredere beeld van waar tijd en bytes heengaan bij zeer grote boeken, zie grote werkmap-prestaties in Delphi

Een HotXLS-vergelijking die een hele-werkblad-lading toont met elke rij resident versus de pull-cursor die alleen de huidige rij plus één formaat-working set vasthoudt, wat PeakRowBufferedBytes accumuleert en rapporteert
PeakRowBufferedBytes accumuleert de huidige uitvoerrij plus de formaatspecifieke working set, dus geheugen volgt hoe breed een rij is in plaats van hoeveel rijen het werkblad heeft

De push-reader werd een adapter, en wat de cursor niet zal doen

TXLSForwardReader draagt niet langer aparte XLSX-, BIFF- en tekstscan-instappunten. Die configureert een cursor, loopt haar af, en vertaalt de huidige rij naar OnSheet- en OnCell-events, en daarom kunnen de twee façades niet meer uit elkaar drijven op filtering, formulestatus of foutafhandeling. Twee gevolgen zijn de moeite van weten waard voordat u upgradet: de callback-SheetIndex is nu uniform 1-based op TXLSForwardReader (TXLSDirectReader behoudt zijn bestaande 0-based eventcontract), en OnSheet gaat af vóór SelectSheet, dus het zetten van SkipSheet betekent dat het sheet-onderdeel helemaal nooit wordt geopend of gedecomprimeerd. De grenzen zijn even expliciet: de werkmap mag niet worden gewijzigd terwijl een pass actief is, annuleren vereist een expliciete herstart, en het BIFF forward-pad decompileert nooit formulertokens, dus klassieke formulecellen melden HasFormula true met FormulaTextAvailable false en geven u het gecachte resultaat in plaats van een lege formulestring te verzinnen. De rijcursor en haar adapter slaagden 1.298 controles op Delphi Win32 en Win64 plus het C++Builder 37.0 Win64 statische pakket

Als u een pull-cursor afweegt tegen de loader die u nu hebt, is de vraag niet welke sneller parseert maar welke u de exit-conditie laat schrijven die u feitelijk nodig hebt. Volledige componentdetails, ondersteunde IDE-versies en licentiëring staan op de HotXLS Delphi spreadsheet component-pagina