HotXLS citește surse .xls, .xlsx, .xlsm, .ods, CSV și TSV printr-un singur cursor pull de rânduri, TXLSRowCursor, al cărui FindFirst și FindNext avansează cu un rând logic pe dată în timp ce doar acel rând rămâne în memorie. O mașină de stări cu șase valori separă before-first de EOF, cancelled și faulted, iar vechiul cititor cu callback e acum un adaptor peste același cursor
Scenariul le e familiar tuturor celor care au livrat o funcție de import. Sosește un .xlsx de 200 MB, conectați un handler OnCell, iar prima cerință după „citește-l” este „oprește-te după primele o sută de postări inversate”. Acum forma codului dumneavoastră vă contrazice: bucla trăiește în interiorul bibliotecii, handlerul dumneavoastră trebuie să ridice un flag, fiecare callback ulterior se declanșează tot până când parserul observă, iar starea acumulată — câte potriviri până acum, ce coloană s-a potrivit, ce urmează — trebuie să trăiască în câmpuri pe o clasă care există doar ca să-i dea callbackului un loc unde să șadă. Nimic din asta nu e o problemă de parsare. E o problemă de control-flow, și e exact cea pe care un cursor pull o elimină
Ce costă de fapt un callback push la 200 MB
Push-ul inversează controlul, iar inversarea e exact ceea ce un apelant care filtrează sau îmbină nu-și poate permite. Cu un API cu callback biblioteca deține bucla, deci apelantul nu poate folosi Break, nu poate intercala două surse, nu poate preda cititorul unei rutine care așteaptă să fie condusă și nu poate exprima „privește rândul următor înainte de a decide” fără buffering. Costul nu e throughput-ul — o cale cu callback SAX bine scrisă face stream fine — ci faptul că fiecare consumator netrivial își crește o mică mașină de stări proprie pentru a simula bucla pe care nu i s-a permis să o scrie. Înmulțiți asta cu patru formate de fișier, fiecare istoric cu propriul punct de intrare de scanare, iar semanticile de filtrare, formulă și eroare încep să derive între ele, exact deriva pe care HotXLS a pornit să o închidă
Cum vă schimbă un cursor pull codul de apel?
Vă redă bucla, și cu ea controlul Pascal obișnuit. TXLSRowCursor.Open acceptă un nume de fișier sau un TStream, detectează formatul, încarcă shared strings și metadate de stil de dată o singură dată și selectează foaia 1. SelectSheet (cu bază unu) sau SelectSheetByName retintizează altă foaie de lucru și resetează cursorul la before-first. FindFirst și FindNext se poziționează apoi pe următorul rând populat — rândurile fără celule decodabile sunt sărite, deci RowIndex poate sări — iar rândul curent e expus ca CellCount, Cells[] și ValueByCol[], toate cu bază unu pe axa coloanelor. Ieșirea din buclă e un Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // sare banda de antet
Cursor.IncludeColumn(1); // decodează doar aceste două coloane
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; // Break obișnuit; niciun flag de abort, nicio sentinelă
end;
until not Cursor.FindNext;
finally
Cursor.Free; // destructorul încheie pasul
end;
end;
Proiecția și intervalul se stabilesc înainte de pas, nu se filtrează după. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates și DetectTextTypes sunt toate respectate în interiorul backendurilor, astfel încât o coloană neselectată nu alocă niciodată valoarea ei, șirul de formulă sau payload-ul rich-text de la bun început — suita de regresie demonstrează asta cu formule de 16 KiB și șiruri din cache care nu sunt niciodată materializate când coloana lor nu e proiectată. Aceste opțiuni sunt înghețate deliberat cât timp un pas e activ și devin din nou inscriptibile la EOF, la SelectSheet sau după Close, astfel încât o scanare să nu poată niciodată amesteca două contracte de decodare. Dacă aveți nevoie doar de inventarul foilor, nu de rânduri, încărcarea doar-metadata și selectivă a foilor e punctul de intrare mai ieftin
Un backend per format, câte o buclă de scanare fiecare
Fiecare format are exact un scanner forward în interiorul HotXLS, iar atât cursorul pull, cât și cititorul cu callback conduc același scanner. TXLSXForwardRowBackend e singura mașină de stări SAX de foaie de lucru pentru părțile de foaie ECMA-376 Part 1 §18.3, deținând cititorul XML, tabela de formule partajate și parserul rich-text, și avansează la exact o graniță fizică <row> per apel. TXLSBiffForwardParser deține globalele, selecția de foaie și avansul de rând pentru fluxul de înregistrări [MS-XLS]; a-l face pauzabil a produs cea mai ascuțită constrângere din tot designul, pentru că o formulă de șir din cache este o înregistrare Formula urmată imediat de o înregistrare String, astfel încât un punct de suspendare per rând nu trebuie să aterizeze niciodată între cele două. TXLSForwardTextBackend ține un cititor conștient de BOM, delimitatorul activ și o înregistrare logică — CSV adulmecă virgulă, punct și virgulă, tab sau pipe din prima înregistrare ignorând caracterele dintre ghilimele, iar câmpurile citate multi-linie sunt unite cu #10 astfel încât numărul rândului să urmărească înregistrările logice, nu newline-urile fizice. TXLSForwardOdsBackend ține un singur șablon de rând fizic pentru tabelele OpenDocument §9, tratează table:number-rows-repeated ca un contor rămas, nu ca o extindere, și avansează peste celulele acoperite fără a emite valori. Cititorul direct de streaming partajează același loader de shared-string și stil de dată
De ce șase stări în loc de un singur flag Eof?
Pentru că un singur boolean face patru situații diferite de nedistins, iar apelanții ghicesc greșit despre toate. TXLSRowCursorState le numește explicit
xrcsClosed— nicio sursă deschisăxrcsBeforeFirst— deschis sau retintizat, încă niciun rând cititxrcsActive— stă pe un rând validxrcsEof— foaia a fost consumată până la capătxrcsCancelled— apelantul a oprit pasul deliberatxrcsFaulted— pasul a eșuat și excepția originală a fost ridicată
Ultima distincție e cea care contează în producție. O parte de foaie lipsă sau un start de pas eșuat își păstrează EReadError-ul și mută cursorul la xrcsFaulted; nu e niciodată retrogradat într-un simplu False pe care un apelant l-ar citi ca „foaia asta era goală”. Cancel e deliberat mai îngust decât Close: închide backendul foii de lucru curente și subfluxul său inflate și invalidează rândul curent, dar nu eliberează arhiva ZIP sau fluxul sursă, iar apelul de două ori e un no-op. După un cancel reiei apelând SelectSheet explicit — cursorul nu va reporni tăcut un pas în numele dumneavoastră. Proprietatea fluxului urmează aceeași regulă defensivă: xsoBorrowed e implicitul și restabilește poziția fluxului la închidere, xsoOwned transferă proprietatea doar după ce Open a reușit deja, astfel încât un open eșuat nu eliberează niciodată un flux pe care apelantul îl deține în continuare
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: cursorul nu eliberează niciodată Src, iar Close restabilește
// poziția pe care fluxul o avea când Open a fost apelat
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // închide backendul foii de lucru și
Break; // doar subfluxul inflate; 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; // încă al nostru, încă valid, poziție restabilită
end;
end;
Împrumutul rândului curent fără a-l copia
IXLSRowCursorView predă un rând altei rutine fără a duplica tabloul de celule. Vederea stochează un guard partajat care deține pointerul cursorului plus un contor de generație UInt64; avansarea, selecția de foaie, anularea, închiderea și distrugerea cursorului incrementează toate acea generație, iar distrugerea golește în plus deținătorul guardului. Deci o vedere învechită nu poate citi memorie eliberată: Valid e o sondă fără excepții pe care o puteți apela oricând, în timp ce fiecare alt membru validează întâi și ridică EXLSRowCursorViewInvalidated. Fiți onest cu privire la ce e acest contract — e fail-fast de durată de viață, nu o garanție de thread-safety, și nu licențiază citirea unui rând dintr-un al doilea fir în timp ce primul avansează cursorul
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // împrumută; niciun tablou de celule nu e copiat
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // citirile BIFF forward păstrează
else if Cell.Kind = xdkEmpty then // rezultatul din cache, nu tokenurile
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank sunt celule reale
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// Interfața supraviețuiește buclei, dar rândul din spatele ei nu
if not View.Valid then // Valid nu ridică niciodată; Cells[] acum ar ridica
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes și ce i se permite să demonstreze
PeakRowBufferedBytes există pentru a demonstra că memoria urmărește lățimea rândului, nu numărul de rânduri. Acumulează înregistrările de celule, Variant-urile, șirurile de formule și payload-urile rich-text ale rândului de ieșire curent și încorporează setul de lucru specific formatului — înregistrarea logică CSV, șablonul de rând fizic ODS, vârful de înregistrări BIFF sau celula brută XLSX în curs de decodare. Citiți-l împreună cu SheetPassesStarted, care numără câte treceri de foaie de lucru au început efectiv. Două avertismente țin asta onest: cifra e o estimare, nu o contabilitate exactă a heap-ului, și e monotonică de la cel mai recent Open, deci e un instrument de depanare și regresie, nu un indicator live. Pentru imaginea mai largă a unde merg timpul și octeții pe registre foarte mari, vedeți performanța registrelor mari în Delphi
Cititorul push a devenit un adaptor și ce nu va face cursorul
TXLSForwardReader nu mai poartă puncte de intrare de scanare separate XLSX, BIFF și text. Configurează un cursor, îl parcurge și traduce rândul curent în evenimente OnSheet și OnCell, motiv pentru care cele două fațade nu se mai pot deriva la filtrare, stare de formulă sau tratare a erorilor. Două consecințe merită știute înainte de upgrade: SheetIndex din callback e acum uniform cu bază unu pe TXLSForwardReader (TXLSDirectReader își păstrează contractul de evenimente existent cu bază zero), iar OnSheet se declanșează înainte de SelectSheet, deci setarea SkipSheet înseamnă că partea de foaie de lucru nu mai e niciodată deschisă sau decomprimată. Granițele sunt la fel de explicite: registrul nu trebuie modificat cât timp un pas e activ, anularea cere o repornire explicită, iar calea BIFF forward nu decompilează niciodată tokenuri de formulă, astfel încât celulele clasice de formulă raportează HasFormula adevărat cu FormulaTextAvailable fals și vă dau rezultatul din cache în loc să inventeze un șir de formulă gol. Cursorul de rânduri și adaptorul său au trecut 1.298 de verificări pe Delphi Win32 și Win64 plus pachetul static C++Builder 37.0 Win64
Dacă cântăriți un cursor pull contra loaderului pe care îl aveți acum, întrebarea de pus nu e care parsează mai repede, ci care vă permite să scrieți condiția de ieșire de care aveți efectiv nevoie. Detaliile complete ale componentei, versiunile IDE suportate și licențierea sunt pe pagina componentei Delphi de foi de calcul HotXLS