Teknisk artikkel

Pull-radmarkør for XLS, XLSX, ODS og CSV i Delphi

HotXLS leser .xls-, .xlsx-, .xlsm-, .ods-, CSV- og TSV-kilder gjennom én enkelt pull-radmarkør, TXLSRowCursor, hvis FindFirst og FindNext flytter én logisk rad om gangen mens bare den raden blir i minnet. En tilstandsmaskin med seks verdier skiller før-første fra EOF, kansellert og feilet, og den eldre callback-leseren er nå en adapter over samme markør

Scenariet er kjent for alle som har levert en importfunksjon. En .xlsx på 200 MB ankommer, du kobler til en OnCell-behandler, og det første kravet etter «les den» er «stopp etter de første hundre reverserte posteringene». Nå jobber formen på koden din mot deg: løkken bor inne i biblioteket, behandleren din må heise et flagg, hvert etterfølgende callback fyres fortsatt til parseren legger merke til det, og den akkumulerte tilstanden — hvor mange treff så langt, hvilken kolonne som matchet, hva som skjer neste — må bo i felt på en klasse som bare finnes for å gi callback-et et sted å sitte. Ingenting av dette er et parserproblem. Det er et kontrollflytproblem, og det er det en pull-markør fjerner

Hva et push-callback faktisk koster ved 200 MB

Push snur kontrollen, og inversjon er akkurat det en filtrerende eller sammenføyende kaller ikke har råd til. Med et callback-API eier biblioteket løkken, så kalleren kan ikke bruke Break, kan ikke flette to kilder, kan ikke gi leseren til en rutine som forventer å bli drevet, og kan ikke uttrykke «kikk på neste rad før du bestemmer» uten buffering. Kostnaden er ikke gjennomstrømning — en velskrevet SAX-callback-sti strømmer fint — det er at hver ikke-triviell forbruker dyrker sin egen lille tilstandsmaskin for å simulere løkken det ikke var tillatt å skrive. Multipliser det med fire filformater, hver historisk med eget skanneinngangspunkt, og filtrerings-, formel- og feilsemantikken begynner å drive fra hverandre mellom dem, som er nøyaktig drivet HotXLS satte seg fore å lukke

Hvordan endrer en pull-markør kallkoden din?

Den gir løkken tilbake til deg, og med den vanlig Pascal-kontrollflyt. TXLSRowCursor.Open godtar et filnavn eller en TStream, oppdager formatet, laster delte strenger og datostilmetadata én gang, og velger ark 1. SelectSheet (1-basert) eller SelectSheetByName sikter mot et annet regneark og nullstiller markøren til før-første. FindFirst og FindNext posisjonerer deretter på neste befolkede rad — rader uten dekbare celler hoppes over, så RowIndex kan hoppe — og den gjeldende raden eksponeres som CellCount, Cells[] og ValueByCol[], alle 1-basert på kolonneaksen. Å forlate løkken er en Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // hopp over topptekstbandet
    Cursor.IncludeColumn(1);     // dekod bare disse to kolonnene
    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;               // vanlig Break; ingen avbrytflagg, ingen sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktoren avslutter passet
  end;
end;

Projeksjon og område settes før passet, ikke filtreres etterpå. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates og DetectTextTypes alle æres inne i backends, så en uvalgt kolonne tildeler aldri sin verdi, formelstreng eller rikt-tekst-nyttelast i det hele tatt — regresjonspakken beviser dette med 16 KiB-formler og bufrede strenger som aldri materialiseres når kolonnen deres ikke er projisert. Disse alternativene er bevisst frosset mens et pass er aktivt og blir skrivbare igjen ved EOF, ved SelectSheet eller etter Close, så én skanning kan aldri blande to dekningskontrakter. Hvis du bare trenger arkinventaret snarere enn radene, er kun-metadata og selektiv arkinnlasting det billigere inngangspunktet

Én backend per format, én skanneløkke hver

Hvert format har nøyaktig én fremover-skanner inne i HotXLS, og både pull-markøren og callback-leseren driver samme skanner. TXLSXForwardRowBackend er den eneste regneark-SAX-tilstandsmaskinen for ECMA-376 Part 1 §18.3 arkdeler, og holder XML-leseren, den delte formeltabellen og rikt-tekst-parseren, og den flytter seg til nøyaktig én fysisk <row>-grense per kall. TXLSBiffForwardParser eier globalene, arkvalget og radfremrykkingen for [MS-XLS]-poststrømmen; å gjøre den pausbar produserte den skarpeste begrensningen i hele designet, fordi en bufret strengformel er en Formula-post umiddelbart etterfulgt av en String-post, så et suspensjonspunkt per rad må aldri lande mellom de to. TXLSForwardTextBackend holder en BOM-bevisst leser, det aktive skilletegnet og én logisk post — CSV snuser komma, semikolon, tabulator eller pipe fra første post mens den ignorerer sitattegn, og flerlinjede siterte felt føyes sammen med #10 slik at radnummeret sporer logiske poster snarere enn fysiske linjeskift. TXLSForwardOdsBackend holder én enkelt fysisk radmal for OpenDocument §9-tabeller, behandler table:number-rows-repeated som et gjenstående antall snarere enn en ekspansjon, og flytter seg forbi dekkede celler uten å emittiere verdier. Strømmende direkteleseren deler samme delte-streng- og datostil-laster

HotXLS pull-radmarkøren som sender til én fremover-skanner per format, en SAX-backend for XLSX, en postparser for BIFF, en skilletegn-snusende tekstbackend og en ODS-radmal, med callback-leseren konfigurert på toppen som en adapter
Hvert format har nøyaktig én fremover-skanner, og både pull-markøren og callback-leseren driver samme skanner, så filtrerings- og feilsemantikk kan ikke drive fra hverandre

Hvorfor seks tilstander i stedet for ett Eof-flagg?

Fordi én enkelt boolsk verdi gjør fire forskjellige situasjoner uatskillelige, og kallere gjetter feil om alle sammen. TXLSRowCursorState navngir dem eksplisitt

  • xrcsClosed — ingen kilde er åpen
  • xrcsBeforeFirst — åpnet eller re-siktet, ingen rad lest ennå
  • xrcsActive — står på en gyldig rad
  • xrcsEof — arket ble konsumert til slutten
  • xrcsCancelled — kalleren stoppet passet bevisst
  • xrcsFaulted — passet feilet og det opprinnelige unntaket ble utløst

Den siste skillelinjen er den som betyr noe i produksjon. En manglende regnearkdel eller en feilet passstart beholder sin EReadError og flytter markøren til xrcsFaulted; den nedgraderes aldri til en ren False som en kaller ville lese som «dette arket var tomt». Cancel er bevisst smalere enn Close: den lukker den gjeldende regneark-backend-en og dens understrøm og ugyldiggjør den gjeldende raden, men den frigir ikke ZIP-arkivet eller kildestrømmen, og å kalle den to ganger er en no-op. Etter en kansellering gjenopptar du ved å kalle SelectSheet eksplisitt — markøren vil ikke stille starte et pass på dine vegne. Strømeierskap følger samme defensive regel: xsoBorrowed er standard og gjenoppretter strømposisjonen ved lukking, xsoOwned overfører eierskap først etter at Open allerede har lykkes, så en feilet åpning frigir aldri en strøm kalleren fortsatt holder

De seks tilstandene til HotXLS-radmarkøren med overgangene mellom dem, som viser Cancel som flytter et aktivt pass til kansellert, en feilet passstart som flytter det til feilet, og hvordan begge forblir distinkte fra slutten av arket
Seks navngitte tilstander holder et tomt ark, et bevisst stopp og et feilet pass atskillelige, noe ett enkelt Eof-flagg ikke kan
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: markøren frigir aldri Src, og Close gjenoppretter
      // posisjonen strømmen hadde da Open ble kalt
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // lukker regneark-backend-en og dens
            Break;           // understrøm bare; 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;                // fortsatt vår, fortsatt gyldig, posisjon gjenopprettet
  end;
end;

Låne gjeldende rad uten å kopiere den

IXLSRowCursorView gir en rad til en annen rutine uten å duplisere cellearrayet. Visningen lagrer en delt vokter som holder markørpekeren pluss en UInt64-generasjonsteller; fremrykking, arkvalg, kansellering, lukking og destruksjon av markøren inkrementerer alle den generasjonen, og destruksjon rydder i tillegg vokter-eieren. Så en foreldet visning kan ikke lese frigitt minne: Valid er en unntaksfri sonde du kan kalle når som helst, mens alle andre medlemmer validerer først og utløser EXLSRowCursorViewInvalidated. Vær ærlig på hva denne kontrakten er — det er levetid fail-fast, ikke en trådsikkerhetsgaranti, og den tillater ikke å lese en rad fra en annen tråd mens den første flytter markøren

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // låner; ingen cellearray kopieres
      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-lesinger beholder det
        else if Cell.Kind = xdkEmpty then //   bufrede resultatet, ikke tokenene
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank er ekte celler
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Grensesnittet overlever løkken, men raden bak det gjør ikke det
  if not View.Valid then    // Valid utløser aldri; Cells[] nå ville utløse
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, og hva den får lov til å bevise

PeakRowBufferedBytes finnes for å demonstrere at minnet sporer radbredde snarere enn radantall. Den akkumulerer cellerecordene, Variantene, formelstrengene og rikt-tekst-nyttelastene til den gjeldende utdataraden og folder inn det formatspesifikke arbeidssettet — den logiske CSV-posten, den fysiske ODS-radmalen, BIFF-posttoppen, eller XLSX-råcellen som dekodes for øyeblikket. Les den sammen med SheetPassesStarted, som teller hvor mange regneark-pass som faktisk begynte. To forbehold holder dette ærlig: tallet er et anslag, ikke nøyaktig heap-regnskap, og det er monotonisk siden siste Open, så det er et feilsøkings- og regresjonsinstrument snarere enn en live-måler. For det bredere bildet av hvor tid og byte går på svært store bøker, se ytelse for store arbeidsbøker i Delphi

En HotXLS-sammenligning som viser en helark-innlasting som holder hver rad boende mot pull-markøren som holder bare den gjeldende raden pluss ett formatarbeidssett, som er det PeakRowBufferedBytes akkumulerer og rapporterer
PeakRowBufferedBytes akkumulerer den gjeldende utdataraden pluss det formatspesifikke arbeidssettet, så minnet sporer hvor bred en rad er snarere enn hvor mange rader arket har

Push-leseren ble en adapter, og hva markøren ikke gjør

TXLSForwardReader bærer ikke lenger separate XLSX-, BIFF- og tekst-skanneinngangspunkter. Den konfigurerer en markør, går gjennom den, og oversetter den gjeldende raden til OnSheet- og OnCell-hendelser, og det er derfor de to fasadene ikke lenger kan drive fra hverandre på filtrering, formeltilstand eller feilhåndtering. To konsekvenser er verdt å vite før du oppgraderer: callback-SheetIndex er nå uniformt 1-basert på TXLSForwardReader (TXLSDirectReader beholder sin eksisterende 0-baserte hendelseskontrakt), og OnSheet fyres før SelectSheet, så å sette SkipSheet betyr at regnearkdelen aldri åpnes eller dekomprimeres i det hele tatt. Grensene er like eksplisitte: arbeidsboken må ikke endres mens et pass er aktivt, kansellering krever en eksplisitt omstart, og BIFF forward-stien dekompilerer aldri formeltokens, så klassiske formelceller rapporterer HasFormula true med FormulaTextAvailable false og gir deg det bufrede resultatet i stedet for å finne på en tom formelstreng. Radmarkøren og dens adapter besto 1 298 sjekker på Delphi Win32 og Win64 pluss C++Builder 37.0 Win64 statisk pakke

Hvis du veier en pull-markør mot lasteren du har nå, er spørsmålet å stille ikke hvilken som parser raskere, men hvilken som lar deg skrive utgangsbetingelsen du faktisk trenger. Fullstendige komponentdetaljer, støttede IDE-versjoner og lisensiering finnes på HotXLS Delphi regnearkkomponent-siden