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
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 öppenxrcsBeforeFirst— öppnad eller omriktad, ingen rad läst ännuxrcsActive— står på en giltig radxrcsEof— arket konsumerades till slutetxrcsCancelled— anroparen stoppade passet avsiktligtxrcsFaulted— 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
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
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