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
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 åbenxrcsBeforeFirst— åbnet eller omsigtet, ingen række læst endnuxrcsActive— står på en gyldig rækkexrcsEof— arket var forbrugt til endenxrcsCancelled— kalderen stoppede passet bevidstxrcsFaulted— 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
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
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