HotXLS legge sorgenti .xls, .xlsx, .xlsm, .ods, CSV e TSV attraverso un unico cursore pull di righe, TXLSRowCursor, il cui FindFirst e FindNext avanzano di una riga logica alla volta mentre solo quella riga resta in memoria. Una macchina a stati a sei valori separa before-first da EOF, cancelled e faulted, e il più vecchio reader a callback è ora un adattatore sopra lo stesso cursore
Lo scenario è familiare a chiunque abbia spedito una funzionalità di importazione. Arriva un .xlsx da 200 MB, cablate un handler OnCell, e il primo requisito dopo "leggilo" è "fermati dopo i primi cento movimenti rovesciati". Ora la forma del vostro codice vi combatte: il loop vive dentro la libreria, il vostro handler deve alzare un flag, ogni callback successivo scatta comunque finché il parser se ne accorge, e lo stato accumulato — quanti hit finora, quale colonna ha matchato, cosa fare dopo — deve vivere in campi su una classe che esiste solo per dare al callback un posto dove sedersi. Nulla di questo è un problema di parsing. È un problema di flusso di controllo, ed è quello che un cursore pull rimuove
Quanto costa davvero una callback push a 200 MB
Il push inverte il controllo, e l'inversione è esattamente ciò che un chiamante che filtra o joina non può permettersi. Con una API a callback la libreria possiede il loop, quindi il chiamante non può usare Break, non può interpolare due sorgenti, non può consegnare il reader a una routine che si aspetta di essere guidata, e non può esprimere "sbircia la riga successiva prima di decidere" senza bufferizzare. Il costo non è il throughput — un percorso a callback SAX ben scritto streama bene — è che ogni consumatore non banale si coltiva una piccola macchina a stati propria per simulare il loop che non gli era permesso scrivere. Moltiplicate questo per quattro formati di file, storicamente ognuno con il proprio punto di ingresso di scansione, e le semantiche di filtro, formula ed errore iniziano a divergere tra loro, che è precisamente la divergenza che HotXLS si proponeva di chiudere
Come cambia un cursore pull il vostro codice chiamante?
Vi restituisce il loop, e con esso il normale flusso di controllo Pascal. TXLSRowCursor.Open accetta un nome file o uno TStream, rileva il formato, carica shared string e metadati di stile data una volta sola, e seleziona il foglio 1. SelectSheet (a base uno) o SelectSheetByName ridirige su un altro worksheet e azzera il cursore a before-first. FindFirst e FindNext poi si posizionano sulla prossima riga popolata — le righe senza celle decodificabili vengono saltate, quindi RowIndex può saltare — e la riga corrente è esposta come CellCount, Cells[] e ValueByCol[], tutti a base uno sull'asse delle colonne. Uscire dal loop è un Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // salta la banda dell'header
Cursor.IncludeColumn(1); // decodifica solo queste due colonne
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 ordinario; niente flag di abort, niente sentinella
end;
until not Cursor.FindNext;
finally
Cursor.Free; // il distruttore termina il pass
end;
end;
Proiezione e intervallo si impostano prima del pass, non si filtrano dopo. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates e DetectTextTypes sono tutti onorati dentro i backend, così una colonna non selezionata non alloca mai in partenza il proprio valore, stringa di formula o payload rich-text — la suite di regression lo dimostra con formule da 16 KiB e stringhe in cache che non vengono mai materializzate quando la loro colonna non è proiettata. Queste opzioni sono deliberatamente congelate mentre un pass è attivo e tornano scrivibili a EOF, su SelectSheet, o dopo Close, così una singola scansione non può mai mescolare due contratti di decodifica. Se vi serve solo l'inventario dei fogli anziché le righe, il caricamento solo metadati e selettivo dei fogli è il punto di ingresso più economico
Un backend per formato, un loop di scansione ciascuno
Ogni formato ha esattamente uno scanner forward dentro HotXLS, e sia il cursore pull sia il reader a callback guidano quello stesso scanner. TXLSXForwardRowBackend è l'unica macchina a stati SAX di worksheet per le parti sheet di ECMA-376 Part 1 §18.3, e trattiene il reader XML, la tabella delle formule condivise e il parser rich-text, avanzando di esattamente un confine fisico <row> per chiamata. TXLSBiffForwardParser possiede i global, la selezione del foglio e l'avanzamento di riga per lo stream di record [MS-XLS]; renderlo pausabile ha prodotto il vincolo più tagliente dell'intero progetto, perché una formula con stringa in cache è un record Formula immediatamente seguito da un record String, quindi un punto di sospensione per riga non deve mai cadere tra i due. TXLSForwardTextBackend conserva un reader consapevole del BOM, il delimitatore attivo e un record logico — il CSV fiuta virgola, punto e virgola, tab o pipe dal primo record ignorando i caratteri tra virgolette, e i campi tra virgolette multi-riga vengono uniti con #10 così il numero di riga segue i record logici anziché i newline fisici. TXLSForwardOdsBackend conserva un singolo template di riga fisica per le tabelle OpenDocument §9, tratta table:number-rows-repeated come un conteggio residuo anziché un'espansione, e avanza oltre le celle coperte senza emettere valori. Il reader diretto in streaming condivide lo stesso loader di shared string e stile data
Perché sei stati invece di un solo flag Eof?
Perché un singolo booleano rende quattro situazioni diverse indistinguibili, e i chiamanti indovinano male su tutte. TXLSRowCursorState le nomina esplicitamente
xrcsClosed— nessuna sorgente è apertaxrcsBeforeFirst— aperto o ridiretto, nessuna riga ancora lettaxrcsActive— in piedi su una riga validaxrcsEof— il foglio è stato consumato fino alla finexrcsCancelled— il chiamante ha fermato il pass deliberatamentexrcsFaulted— il pass è fallito e l'eccezione originale è stata sollevata
Quell'ultima distinzione è quella che conta in produzione. Una parte di worksheet mancante o un inizio di pass fallito conserva il proprio EReadError e porta il cursore a xrcsFaulted; non viene mai degradato in un semplice False che un chiamante leggerebbe come "questo foglio era vuoto". Cancel è deliberatamente più stretto di Close: chiude il backend del worksheet corrente e il suo substream inflate e invalida la riga corrente, ma non rilascia l'archivio ZIP né lo stream sorgente, e chiamarlo due volte è un no-op. Dopo un cancel riprendete chiamando esplicitamente SelectSheet — il cursore non riavvierà quietly un pass per vostro conto. La proprietà degli stream segue la stessa regola difensiva: xsoBorrowed è il default e ripristina la posizione dello stream alla chiusura, xsoOwned trasferisce la proprietà solo dopo che Open è già riuscito, così un open fallito non libera mai uno stream che il chiamante trattiene ancora
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: il cursore non libera mai Src, e Close ripristina la
// posizione che lo stream aveva quando Open è stato chiamato
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // chiude il backend del worksheet e il suo
Break; // substream inflate soltanto; idempotente
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; // ancora nostra, ancora valida, posizione ripristinata
end;
end;
Prendere in prestito la riga corrente senza copiarla
IXLSRowCursorView consegna una riga a un'altra routine senza duplicare l'array di celle. La vista conserva una guardia condivisa che tiene il puntatore del cursore più un contatore di generazione UInt64; avanzare, selezionare un foglio, cancellare, chiudere e distruggere il cursore incrementano tutti quella generazione, e la distruzione in più azzera il proprietario della guardia. Così una vista stantia non può leggere memoria liberata: Valid è una sonda senza eccezioni che potete chiamare in qualsiasi momento, mentre ogni altro membro valida prima e solleva EXLSRowCursorViewInvalidated. Siate onesti su cosa sia questo contratto — è fail-fast sul lifetime, non una garanzia di thread-safety, e non autorizza a leggere una riga da un secondo thread mentre il primo avanza il cursore
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // prende in prestito; nessun array di celle copiato
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // le letture forward BIFF conservano il
else if Cell.Kind = xdkEmpty then // risultato in cache, non i token
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank sono celle reali
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// L'interfaccia sopravvive al loop, ma la riga dietro di essa no
if not View.Valid then // Valid non lancia mai; Cells[] ora lancerebbe
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes, e cosa gli è permesso dimostrare
PeakRowBufferedBytes esiste per dimostrare che la memoria segue la larghezza della riga anziché il numero di righe. Accumula i record di cella, i Variant, le stringhe di formula e i payload rich-text della riga di output corrente e incorpora il working set specifico del formato — il record logico CSV, il template di riga fisica ODS, il picco di record BIFF, o la cella raw XLSX attualmente in decodifica. Leggetelo insieme a SheetPassesStarted, che conta quanti pass di worksheet sono effettivamente iniziati. Due avvertenze lo tengono onesto: la cifra è una stima, non una contabilità heap esatta, ed è monotona dall'Open più recente, quindi è uno strumento di debugging e regression anziché un misuratore in tempo reale. Per il quadro più ampio di dove vanno tempo e byte su libri molto grandi, vedere le prestazioni su cartelle di lavoro grandi in Delphi
Il reader push è diventato un adattatore, e cosa il cursore non farà
TXLSForwardReader non porta più punti di ingresso di scansione separati per XLSX, BIFF e testo. Configura un cursore, lo percorre, e traduce la riga corrente negli eventi OnSheet e OnCell, ed è per questo che le due facciate non possono più divergere su filtro, stato delle formule o gestione degli errori. Due conseguenze valgono la pena di essere note prima di aggiornare: la callback SheetIndex è ora uniformemente a base uno su TXLSForwardReader (TXLSDirectReader conserva il suo esistente contratto di eventi a base zero), e OnSheet scatta prima di SelectSheet, quindi impostare SkipSheet significa che la parte del worksheet non viene mai aperta o decompressa affatto. I confini sono altrettanto espliciti: la cartella di lavoro non va modificata mentre un pass è attivo, la cancellazione richiede un riavvio esplicito, e il percorso forward BIFF non decompila mai i token delle formule, così le celle formula classic riportano HasFormula true con FormulaTextAvailable false e consegnano il risultato in cache invece di inventare una stringa di formula vuota. Il cursore di righe e il suo adattatore hanno passato 1.298 controlli su Delphi Win32 e Win64 più il pacchetto statico C++Builder 37.0 Win64
Se state pesando un cursore pull contro il loader che avete ora, la domanda da porsi non è quale parsi più veloce ma quale vi lascia scrivere la condizione di uscita di cui avete davvero bisogno. Dettagli completi del componente, versioni IDE supportate e licenze sono sulla pagina del componente foglio di calcolo HotXLS per Delphi