Teknisk artikel

Pull-radmarkör för XLS, XLSX, ODS och CSV i Delphi

HotXLS läser källor i .xls, .xlsx, .xlsm, .ods, CSV och TSV genom en enda pull-radmarkör, TXLSRowCursor, vars FindFirst och FindNext avancerar en logisk rad i taget medan bara den raden ligger kvar i minnet. En tillståndsmaskin med sex värden skiljer before-first från EOF, avbruten och felaktig, och den äldre callback-läsaren är nu en adapter över samma markör

Scenariot är bekant för alla som har skeppat en importfunktion. En 200 MB .xlsx anländer, du kopplar upp en OnCell-hanterare, och det första kravet efter ”läs den” är ”stoppa efter de första hundra korrigeringsposterna”. Nu kämpar din kods form emot dig: loopen bor inuti biblioteket, din hanterare måste resa en flagga, varje efterföljande callback avfyras fortfarande tills parsern märker det, och det ackumulerade tillståndet — hur många träffar hittills, vilken kolumn matchade, vad som ska göras härnäst — måste bo i fält på en klass som bara existerar för att ge callbacken någonstans att sitta. Inget av det är ett parsningsproblem. Det är ett kontrollflödesproblem, och det är det som en pull-markör tar bort

Vad en push-callback faktiskt kostar vid 200 MB

Push inverterar kontrollen, och inversionen är exakt vad en filtrerande eller joinande anropare inte har råd med. Med ett callback-API äger biblioteket loopen, så anroparen kan inte använda Break, kan inte sammanfläta två källor, kan inte lämna läsaren till en rutin som förväntar sig att bli driven, och kan inte uttrycka ”kika på nästa rad innan beslut” utan buffring. Kostnaden är inte genomströmning — en välskriven SAX-callbackväg strömmar utmärkt — det är att varje icke-trivial konsument växer en liten egen tillståndsmaskin för att simulera loopen den inte fick skriva. Multiplicera det med fyra filformat, var och en historiskt med sin egen skanningsingångspunkt, och semantiken för filtrering, formler och fel börjar driva isär dem emellan, vilket är precis den drift HotXLS satte sig för att stänga

Hur förändrar en pull-markör din anropskod?

Den ger tillbaka loopen till dig, och med den vanligt Pascal-kontrollflöde. TXLSRowCursor.Open accepterar ett filnamn eller en TStream, detekterar formatet, laddar delade strängar och datumstilmetadata en gång, och väljer ark 1. SelectSheet (1-baserad) eller SelectSheetByName riktar om till ett annat kalkylark och återställer markören till before-first. FindFirst och FindNext positionerar sedan på nästa ifyllda rad — rader utan avkodningsbara celler hoppas över, så RowIndex kan hoppa — och den aktuella raden exponeras som CellCount, Cells[] och ValueByCol[], alla 1-baserade på kolumnaxeln. Att lämna loopen är ett Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // hoppa över rubrikbandet
    Cursor.IncludeColumn(1);     // avkoda bara dessa två kolumner
    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;               // vanligt Break; ingen avbrytsflagga, ingen sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktorn avslutar passet
  end;
end;

Projektion och intervall sätts före passet, inte filtreras efteråt. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates och DetectTextTypes respekteras alla inuti backendarna, så en ovald kolumn allokerar aldrig sitt värde, sin formelsträng eller sin rich-text-nyttolast från första början — regressionssviten bevisar detta med 16 KiB-formler och cachade strängar som aldrig materialiseras när deras kolumn inte projiceras. Dessa alternativ är avsiktligt frysta medan ett pass är aktivt och blir skrivbara igen vid EOF, vid SelectSheet eller efter Close, så en skanning kan aldrig blanda två avkodningskontrakt. Om du bara behöver arkinventariet snarare än raderna är enbart metadata och selektiv arkladdning den billigare ingångspunkten

En backend per format, en skanningsloop vardera

Varje format har exakt en framåtskanner inuti HotXLS, och både pull-markören och callback-läsaren driver samma skanner. TXLSXForwardRowBackend är den enda SAX-tillståndsmaskinen för kalkylark för ECMA-376 Part 1 §18.3 arkdelar, och håller XML-läsaren, tabellen för delade formler och rich-text-parsern, och den avancerar till exakt en fysisk <row>-gräns per anrop. TXLSBiffForwardParser äger globalerna, arkvalet och radavancemanget för [MS-XLS]-postströmmen; att göra den pausbar producerade den skarpaste begränsningen i hela designen, eftersom en cachad strängformel är en Formula-post omedelbart följd av en String-post, så en avbrottspunkt per rad får aldrig landa mellan de två. TXLSForwardTextBackend håller en BOM-medveten läsare, den aktiva avgränsaren och en logisk post — CSV sniffar komma, semikolon, tab eller pipe från den första posten medan den ignorerar citerade tecken, och flerradiga citerade fält fogas med #10 så att radnumret följer logiska poster snarare än fysiska radbrytningar. TXLSForwardOdsBackend håller en enda fysisk radmall för OpenDocument §9-tabeller, behandlar table:number-rows-repeated som ett återstående antal snarare än en expansion, och avancerar förbi täckta celler utan att emittera värden. Den strömmande direktläsaren delar samma laddare för delade strängar och datumstilar

HotXLS pull-radmarkör som skickar vidare till en framåtskanner per format, en SAX-backend för XLSX, en postparser för BIFF, en avgränsarsniffande textbackend och en ODS-radmall, med callback-läsaren konfigurerad ovanpå som en adapter
Varje format har exakt en framåtskanner, och både pull-markören och callback-läsaren driver samma skanner, så filtrerings- och felsemantik kan inte driva isär

Varför sex tillstånd i stället för en Eof-flagga?

Eftersom en enda boolean gör fyra olika situationer oskiljbara, och anropare gissar fel på dem alla. TXLSRowCursorState namnger dem explicit

  • xrcsClosed — ingen källa är öppen
  • xrcsBeforeFirst — öppnad eller omriktad, ingen rad läst ännu
  • xrcsActive — står på en giltig rad
  • xrcsEof — arket konsumerades till slutet
  • xrcsCancelled — anroparen stoppade passet avsiktligt
  • xrcsFaulted — passet misslyckades och det ursprungliga undantaget kastades

Den sista distinktionen är den som spelar roll i produktion. En saknad kalkylarksdel eller en misslyckad passstart behåller sin EReadError och flyttar markören till xrcsFaulted; den nedgraderas aldrig till ett vanligt False som en anropare skulle läsa som ”det här arket var tomt”. Cancel är avsiktligt snävare än Close: den stänger den aktuella kalkylarksbackenden och dess inflate-underström och ogiltigförklarar den aktuella raden, men den frigör inte ZIP-arkivet eller källströmmen, och att anropa den två gånger är en no-op. Efter en cancel återupptar du genom att anropa SelectSheet explicit — markören startar inte tyst om ett pass för din räkning. Strömmens ägarskap följer samma defensiva regel: xsoBorrowed är default och återställer strömpositionen vid stängning, xsoOwned överför ägarskap först efter att Open redan har lyckats, så en misslyckad öppning frigör aldrig en ström som anroparen fortfarande håller

De sex tillstånden i HotXLS radmarkör med övergångarna mellan dem, som visar hur Cancel flyttar ett aktivt pass till avbrutet, en misslyckad passstart flyttar det till felaktigt, och hur båda förblir skilda från arkets slut
Sex namngivna tillstånd håller ett tomt ark, ett avsiktligt stopp och ett misslyckat pass särskiljbara, vilket en enda Eof-boolean inte kan
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: markören frigör aldrig Src, och Close återställer
      // positionen strömmen hade när Open anropades
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // stänger bara kalkylarksbackenden och dess
            Break;           // inflate-underström; 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;                // fortfarande vår, fortfarande giltig, position återställd
  end;
end;

Låna den aktuella raden utan att kopiera den

IXLSRowCursorView lämnar en rad till en annan rutin utan att duplicera cellarrayen. Vyn lagrar en delad vakt som håller markörpekaren plus en UInt64-generationsräknare; avancera, välja ark, avbryta, stänga och förstöra markören inkrementerar alla den generationen, och förstörelsen rensar dessutom vaktägaren. Så en föråldrad vy kan inte läsa frigjort minne: Valid är en undantagsfri sondering du kan anropa när som helst, medan varje annan medlem validerar först och kastar EXLSRowCursorViewInvalidated. Var ärlig om vad detta kontrakt är — det är livstids-fail-fast, inte en trådsäkerhetsgaranti, och det licensierar inte läsning av en rad från en andra tråd medan den första avancerar markören

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // lånar; ingen cellarray kopieras
      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 framåtläsningar behåller det
        else if Cell.Kind = xdkEmpty then //   cachade resultatet, inte tokensen
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank är riktiga celler
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Interfacet överlever loopen, men raden bakom det gör det inte
  if not View.Valid then    // Valid kastar aldrig; Cells[] nu skulle kasta
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, och vad den får bevisa

PeakRowBufferedBytes finns för att demonstrera att minnet följer radbredd snarare än radantal. Den ackumulerar cellposterna, Varianterna, formelsträngarna och rich-text-nyttolasterna i den aktuella utdataraden och viker in den formatspecifika arbetsmängden — den logiska CSV-posten, ODS:s fysiska radmall, BIFF-posttoppen eller den XLSX-råcell som avkodas just nu. Läs den tillsammans med SheetPassesStarted, som räknar hur många kalkylarkspass som faktiskt påbörjades. Två brasklappar håller detta ärligt: siffran är en uppskattning, inte exakt heapredovisning, och den är monoton sedan den senaste Open, så den är ett felsöknings- och regressionsinstrument snarare än en levande mätare. För den bredare bilden av var tid och byte tar vägen på mycket stora böcker, se prestanda för stora arbetsböcker i Delphi

En HotXLS-jämförelse som visar en hel-arkladdning som håller varje rad resident mot pull-markören som bara håller den aktuella raden plus en formats arbetsmängd, vilket är vad PeakRowBufferedBytes ackumulerar och rapporterar
PeakRowBufferedBytes ackumulerar den aktuella utdataraden plus den formatspecifika arbetsmängden, så minnet följer hur bred en rad är snarare än hur många rader arket har

Push-läsaren blev en adapter, och vad markören inte gör

TXLSForwardReader bär inte längre separata skanningsingångspunkter för XLSX, BIFF och text. Den konfigurerar en markör, går igenom den och översätter den aktuella raden till OnSheet- och OnCell-händelser, vilket är därför de två fasaderna inte längre kan driva isär på filtrering, formeltillstånd eller felhantering. Två konsekvenser är värda att känna till innan du uppgraderar: callback-SheetIndex är nu enhetligt 1-baserad på TXLSForwardReader (TXLSDirectReader behåller sitt befintliga 0-baserade händelsekontrakt), och OnSheet avfyras före SelectSheet, så att sätta SkipSheet betyder att kalkylarksdelen aldrig öppnas eller dekomprimeras alls. Gränserna är lika explicita: arbetsboken får inte modifieras medan ett pass är aktivt, avbrytande kräver en explicit omstart, och BIFF-framåtvägen dekompilerar aldrig formeltokens, så klassiska formelceller rapporterar HasFormula true med FormulaTextAvailable false och lämnar dig det cachade resultatet i stället för att hitta på en tom formelsträng. Radmarkören och dess adapter klarade 1 298 kontroller på Delphi Win32 och Win64 plus det statiska paketet C++Builder 37.0 Win64

Om du väger en pull-markör mot den laddare du har nu är frågan att ställa inte vilken som parsar snabbare utan vilken som låter dig skriva det avslutningsvillkor du faktiskt behöver. Fullständig komponentinformation, IDE-versioner som stöds och licensiering finns på sidan för HotXLS Delphi-kalkylarkskomponent