Teknisk artikel

Pull row-cursor til XLS, XLSX, ODS og CSV i Delphi

HotXLS læser .xls-, .xlsx-, .xlsm-, .ods-, CSV- og TSV-kilder gennem én enkelt pull row-cursor, TXLSRowCursor, hvis FindFirst og FindNext rykker én logisk række ad gangen, mens kun dén række er i hukommelsen. En tilstandsmaskine med seks værdier adskiller before-first fra EOF, cancelled og faulted, og den ældre callback-læser er nu en adapter over samme cursor

Scenariet er kendt for alle, der har afskibet en importfunktion. En 200 MB .xlsx ankommer, du kobler en OnCell-handler på, og det første krav efter læs den er stop efter de første hundrede omvendte posteringer. Nu kæmper kodens form imod dig: løkken bor inde i biblioteket, din handler skal rejse et flag, hvert efterfølgende callback udløses stadig, indtil parseren bemærker det, og den akkumulerede tilstand — hvor mange træffere indtil videre, hvilken kolonne matchede, hvad gør vi nu — skal bo i felter på en klasse, der kun findes for at give callbacket et sted at sidde. Intet af det er et parseproblem. Det er et kontrolflowproblem, og det er det, en pull-cursor fjerner

Hvad et push-callback reelt koster ved 200 MB

Push vender kontrollen, og inversion er præcis det, en filtrerende eller sammenfattende kalder ikke har råd til. Med et callback-API ejer biblioteket løkken, så kalderen kan ikke bruge Break, kan ikke flette to kilder, kan ikke overrække læseren til en rutine, der forventer at blive drevet, og kan ikke udtrykke kig på næste række, før du beslutter dig, uden buffering. Omkostningen er ikke gennemløbshastighed — en velskrevet SAX-callback-sti streamer fint — det er, at hver ikke-triviel forbruger vokser sin egen lille tilstandsmaskine for at simulere den løkke, den ikke måtte skrive. Gang det med fire filformater, der historisk hver har sit eget scanningindgangspunkt, og filtrerings-, formel- og fejlsemantikken begynder at drive fra hinanden mellem dem, hvilket præcis er den drift, HotXLS satte sig for at lukke

Hvordan ændrer en pull-cursor din kaldende kode?

Den giver løkken tilbage til dig, og med den almindeligt Pascal-kontrolflow. TXLSRowCursor.Open accepterer et filnavn eller en TStream, detekterer formatet, indlæser delte strenge og datostil-metadata én gang og vælger ark 1. SelectSheet (enbaseret) eller SelectSheetByName sigter om på et andet regneark og nulstiller cursoren til before-first. FindFirst og FindNext positionerer derefter på næste befolkede række — rækker uden afkodbare celler springes over, så RowIndex kan hoppe — og den aktuelle række eksponeres som CellCount, Cells[] og ValueByCol[], alle enbaserede på kolonneaksen. At forlade løkken er et Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // spring headerbåndet over
    Cursor.IncludeColumn(1);     // afkod kun disse to kolonner
    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;               // almindeligt Break; intet abort-flag, ingen sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktoren afslutter passet
  end;
end;

Projektion og interval sættes før passet, ikke filtreres bagefter. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates og DetectTextTypes honderes alle inde i backenderne, så en ikke-valgt kolonne aldrig allokerer sin værdi, formelstreng eller rich-text-nyttelast i første omgang — regressionssuiten beviser det med 16 KiB-formler og cachede strenge, der aldrig materialiseres, når deres kolonne ikke er projiceret. De indstillinger er bevidst frosset, mens et pass er aktivt, og bliver skrivbare igen ved EOF, ved SelectSheet eller efter Close, så én scanning aldrig kan blande to afkodningskontrakter. Hvis du kun behøver arkinventaret frem for rækkerne, er metadata-only og selektiv arkindlæsning det billigere indgangspunkt

Én backend pr. format, én scanningløkke hver

Hvert format har præcis én fremadrettet scanner inde i HotXLS, og både pull-cursoren og callback-læseren driver den samme scanner. TXLSXForwardRowBackend er den eneste regnearks-SAX-tilstandsmaskine for ECMA-376 Part 1 §18.3-sheetparts og holder XML-læseren, tabellen over delte formler og rich-text-parseren, og den rykker til præcis én fysisk <row>-grænse pr. kald. TXLSBiffForwardParser ejer globalsene, arkvalget og rækkefremrykningen for [MS-XLS]-recordstrømmen; at gøre den pausabel gav den skarpeste begrænsning i hele designet, for en cached strengformel er en Formula-record umiddelbart efterfulgt af en String-record, så et ophængspunkt pr. række må aldrig lande mellem de to. TXLSForwardTextBackend holder en BOM-bevidst læser, den aktive afgrænser og én logisk record — CSV sniffer komma, semikolon, tab eller pipe fra den første record, mens den ignorerer citerede tegn, og flerlinjede citerede felter sammenføjes med #10, så rækkenummeret følger logiske records frem for fysiske linjeskift. TXLSForwardOdsBackend holder én enkelt fysisk rækkeskabelon for OpenDocument §9-tabeller, behandler table:number-rows-repeated som en resterende-antal-tæller frem for en ekspansion og rykker forbi dækkede celler uden at emitte værdier. Streaming direct reader deler samme loader til delte strenge og datostile

HotXLS pull row-cursoren, der afsender til én fremadrettet scanner pr. format — en SAX-backend til XLSX, en recordparser til BIFF, en afgrænser-sniffende tekstbackend og en ODS-rækkeskabelon — med callback-læseren konfigureret ovenpå som en adapter
Hvert format har præcis én fremadrettet scanner, og både pull-cursoren og callback-læseren driver den samme scanner, så filtrerings- og fejlsemantik ikke kan drive fra hinanden

Hvorfor seks tilstande i stedet for ét Eof-flag?

Fordi én enkelt boolean gør fire forskellige situationer uadskillelige, og kaldere gætter forkert om alle. TXLSRowCursorState navngiver dem eksplicit

  • xrcsClosed — ingen kilde er åben
  • xrcsBeforeFirst — åbnet eller omsigtet, ingen række læst endnu
  • xrcsActive — står på en gyldig række
  • xrcsEof — arket var forbrugt til enden
  • xrcsCancelled — kalderen stoppede passet bevidst
  • xrcsFaulted — passet fejlede, og den oprindelige undtagelse blev rejst

Den sidste skelnen er den, der tæller i produktion. En manglende worksheetpart eller et fejlet pass-start beholder sin EReadError og flytter cursoren til xrcsFaulted; den degraderes aldrig til en almindelig False, som en kalder ville læse som dette ark var tomt. Cancel er bevidst snævrere end Close: den lukker den aktuelle worksheet-backend og dens inflate-substream og invaliderer den aktuelle række, men den frigiver ikke ZIP-arkivet eller kildestrømmen, og at kalde den to gange er en no-op. Efter en cancel genoptager du ved eksplicit at kalde SelectSheet — cursoren genstarter ikke stille et pass på dine vegne. Streamejerskab følger samme defensive regel: xsoBorrowed er default og gendanner strømpositionen ved lukning, xsoOwned overfører ejerskab først, efter Open allerede er lykkedes, så en fejlet åbning aldrig frigiver en strøm, kalderen stadig holder

HotXLS rækkecursorens seks tilstande med overgangene imellem, som viser Cancel flytte et aktivt pass til cancelled, et fejlet pass-start flytte det til faulted, og hvordan begge forbliver adskilt fra arkets ende
Seks navngivne tilstande holder et tomt ark, et bevidst stop og et fejlet pass adskillelige, hvilket ét enkelt Eof-flag ikke kan
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: cursoren frigiver aldrig Src, og Close gendanner den
      // position, strømmen havde, da Open blev kaldt
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // lukker worksheet-backenden og dens
            Break;           // inflate-substream alene; 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;                // stadig vores, stadig gyldig, position gendannet
  end;
end;

At låne den aktuelle række uden at kopiere den

IXLSRowCursorView overrækker en række til en anden rutine uden at duplikere cellearrayet. Viewet gemmer en delt vagt, der holder cursorpointeren plus en UInt64-generationstæller; fremrykning, arkvælg, cancel, lukning og destruktion af cursoren øger alle den generation, og destruktion rydder derudover vagtens ejer. Så et forældet view kan ikke læse frigivet hukommelse: Valid er en undtagelsesfri sonde, du kan kalde når som helst, mens alle andre medlemmer validerer først og udløser EXLSRowCursorViewInvalidated. Vær ærlig om, hvad denne kontrakt er — det er lifetime fail-fast, ikke en trådsikkerhedsgaranti, og den giver ikke licens til at læse en række fra en anden tråd, mens den første fremrykker cursoren

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // låner; intet 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-læsninger beholder
        else if Cell.Kind = xdkEmpty then //   det cachede resultat, ikke tokens
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank er rigtige celler
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Interfacet overlever løkken, men rækken bag det gør ikke
  if not View.Valid then    // Valid udløser aldrig; Cells[] ville nu udløse
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, og hvad det må bevise

PeakRowBufferedBytes findes for at demonstrere, at hukommelsen følger rækkebredden frem for rækkeantallet. Den akkumulerer cellerecordene, Variants, formelstrenge og rich-text-nyttelaster for den aktuelle outputrække og folder det formatspecifikke arbejdssæt ind — CSV-logikrecorden, ODS-fysisk rækkeskabelon, BIFF-record-toppen eller den XLSX-råcelle, der aktuelt afkodes. Læs den sammen med SheetPassesStarted, som tæller, hvor mange worksheet-passes der faktisk begyndte. To forbehold holder den ærlig: tallet er et estimat, ikke præcis heap-regnskab, og det er monotont siden seneste Open, så det er et fejlfindings- og regressionsinstrument frem for en live-måler. For det bredere billede af, hvor tid og bytes går på meget store bøger, se ydeevne for store arbejdsbøger i Delphi

En HotXLS-sammenligning, der viser en helark-indlæsning holde hver række resident mod pull-cursoren holde kun den aktuelle række plus ét formats arbejdssæt, hvilket er det, PeakRowBufferedBytes akkumulerer og rapporterer
PeakRowBufferedBytes akkumulerer den aktuelle outputrække plus det formatspecifikke arbejdssæt, så hukommelsen følger, hvor bred en række er, frem for hvor mange rækker arket har

Push-læseren blev en adapter, og hvad cursoren ikke gør

TXLSForwardReader bærer ikke længere separate XLSX-, BIFF- og tekstscanningindgangspunkter. Den konfigurerer en cursor, gennemløber den og oversætter den aktuelle række til OnSheet- og OnCell-hændelser, hvilket er grunden til, at de to facader ikke længere kan drive fra hinanden på filtrering, formeltilstand eller fejlhåndtering. To konsekvenser er værd at kende, før du opgraderer: callback SheetIndex er nu ensartet enbaseret på TXLSForwardReader (TXLSDirectReader beholder sin eksisterende nulbaserede hændelseskontrakt), og OnSheet udløses før SelectSheet, så at sætte SkipSheet betyder, at worksheetparten aldrig åbnes eller dekomprimeres overhovedet. Grænserne er lige så eksplicitte: arbejdsbogen må ikke ændres, mens et pass er aktivt, aflysning behøver en eksplicit genstart, og BIFF-forward-stien dekompilerer aldrig formeltokens, så klassiske formelceller rapporterer HasFormula true med FormulaTextAvailable false og overrækker det cachede resultat i stedet for at opfinde en tom formelstreng. Rækkecursoren og dens adapter bestod 1.298 kontroller på Delphi Win32 og Win64 plus C++Builder 37.0 Win64 static package

Hvis du vejer en pull-cursor op mod den loader, du har nu, er spørgsmålet ikke, hvilken der parser hurtigst, men hvilken der lader dig skrive den exit-betingelse, du faktisk behøver. Fuld komponentdetaljer, understøttede IDE-versioner og licensering er på HotXLS Delphi spreadsheet component-siden