O HotXLS lê fontes .xls, .xlsx, .xlsm, .ods, CSV e TSV por meio de um único cursor pull de linhas, o TXLSRowCursor, cujos FindFirst e FindNext avançam uma linha lógica por vez enquanto só essa linha permanece em memória. Uma máquina de estados de seis valores separa before-first de EOF, cancelled e faulted, e o leitor callback mais antigo agora é um adaptador sobre o mesmo cursor
O cenário é familiar para qualquer pessoa que já entregou uma funcionalidade de importação. Chega um .xlsx de 200 MB, você liga um handler OnCell, e o primeiro requisito depois de "ler isso" é "parar após as primeiras cem postagens revertidas". Agora a forma do seu código luta contra você: o loop vive dentro da biblioteca, seu handler precisa levantar uma flag, todo callback subsequente ainda dispara até o parser notar, e o estado acumulado — quantos acertos até agora, qual coluna correspondeu, o que fazer em seguida — precisa viver em fields de uma classe que existe apenas para dar um lugar ao callback. Nada disso é um problema de parsing. É um problema de control flow, e é justamente o que um cursor pull remove
O que um callback push realmente custa a 200 MB
Push inverte o controle, e inversão é exatamente o que um chamador que filtra ou faz join não pode pagar. Com uma API de callback a biblioteca é dona do loop, então o chamador não pode usar Break, não pode intercalar duas fontes, não pode entregar o reader a uma rotina que espera ser dirigida, e não pode expressar "espiar a próxima linha antes de decidir" sem fazer buffer. O custo não é throughput — um caminho de callback SAX bem escrito faz stream tranquilamente — é que todo consumidor não trivial cresce uma pequena máquina de estados própria para simular o loop que não tinha permissão de escrever. Multiplique isso por quatro formatos de arquivo, cada um historicamente com seu próprio ponto de entrada de varredura, e as semânticas de filtragem, fórmula e erro começam a divergir entre eles, que é precisamente a divergência que o HotXLS se propôs a fechar
Como um cursor pull muda seu código de chamada?
Ele devolve o loop a você, e com ele o control flow Pascal comum. TXLSRowCursor.Open aceita um nome de arquivo ou um TStream, detecta o formato, carrega shared strings e metadados de estilo de data uma vez, e seleciona a sheet 1. SelectSheet (base 1) ou SelectSheetByName mira outra worksheet e redefine o cursor para before-first. FindFirst e FindNext então se posicionam na próxima linha povoada — linhas sem células decodificáveis são puladas, então RowIndex pode saltar — e a linha atual é exposta como CellCount, Cells[] e ValueByCol[], todos com base 1 no eixo de colunas. Sair do loop é um Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // pula a faixa de cabeçalho
Cursor.IncludeColumn(1); // decodifica só estas duas colunas
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 comum; sem flag de aborto, sem sentinela
end;
until not Cursor.FindNext;
finally
Cursor.Free; // o destructor encerra o passe
end;
end;
Projeção e faixa são definidas antes do passe, não filtradas depois. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates e DetectTextTypes são todos honrados dentro dos backends, então uma coluna não selecionada nunca aloca seu valor, string de fórmula ou payload de rich-text em primeiro lugar — a suíte de regressão prova isso com fórmulas de 16 KiB e strings em cache que nunca são materializadas quando sua coluna não é projetada. Essas opções são deliberadamente congeladas enquanto um passe está ativo e voltam a ser graváveis no EOF, em SelectSheet, ou após Close, então uma varredura nunca pode misturar dois contratos de decodificação. Se você só precisa do inventário de sheets em vez das linhas, carregamento apenas de metadados e seletivo de sheets é o ponto de entrada mais barato
Um backend por formato, um loop de varredura cada
Cada formato tem exatamente um scanner forward dentro do HotXLS, e tanto o cursor pull quanto o leitor callback dirigem esse mesmo scanner. TXLSXForwardRowBackend é a única máquina de estados SAX de worksheet para sheet parts da ECMA-376 Part 1 §18.3, guardando o leitor XML, a tabela de shared formulas e o parser de rich-text, e avança até exatamente um limite físico de <row> por chamada. TXLSBiffForwardParser é dono dos globals, da seleção de sheet e do avanço de linha para o stream de records do [MS-XLS]; torná-lo pausável produziu a restrição mais afiada de todo o design, porque uma fórmula com string em cache é um record Formula imediatamente seguido por um record String, então um ponto de suspensão por linha nunca pode cair entre os dois. TXLSForwardTextBackend mantém um leitor consciente de BOM, o delimitador ativo e um record lógico — CSV detecta comma, semicolon, tab ou pipe a partir do primeiro record enquanto ignora caracteres entre aspas, e campos entre aspas com múltiplas linhas são unidos com #10 para que o número da linha acompanhe records lógicos em vez de newlines físicas. TXLSForwardOdsBackend mantém um único template de linha física para tabelas do OpenDocument §9, trata table:number-rows-repeated como uma contagem restante em vez de uma expansão, e avança além das células cobertas sem emitir valores. O streaming direct reader compartilha o mesmo carregador de shared strings e estilos de data
Por que seis estados em vez de uma flag Eof?
Porque um único booleano torna quatro situações diferentes indistinguíveis, e os chamadores erram o palpite sobre todas. TXLSRowCursorState as nomeia explicitamente
xrcsClosed— nenhuma fonte está abertaxrcsBeforeFirst— aberto ou re-mirado, nenhuma linha lida aindaxrcsActive— parado sobre uma linha válidaxrcsEof— a sheet foi consumida até o fimxrcsCancelled— o chamador parou o passe deliberadamentexrcsFaulted— o passe falhou e a exceção original foi levantada
A última distinção é a que importa em produção. Uma worksheet part ausente ou um início de passe que falha mantém seu EReadError e move o cursor para xrcsFaulted; nunca é rebaixado a um False simples que o chamador leria como "esta sheet estava vazia". Cancel é deliberadamente mais estreito que Close: ele fecha o backend da worksheet atual e seu substream de inflate e invalida a linha atual, mas não libera o arquivo ZIP nem o stream de origem, e chamá-lo duas vezes é um no-op. Depois de um cancel você retoma chamando SelectSheet explicitamente — o cursor não reiniciará um passe silenciosamente por você. A posse do stream segue a mesma regra defensiva: xsoBorrowed é o padrão e restaura a posição do stream ao fechar, xsoOwned transfere a posse somente depois que Open já teve sucesso, então um open que falha nunca libera um stream que o chamador ainda mantém
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: o cursor nunca libera Src, e o Close restaura a
// posição que o stream tinha quando Open foi chamado
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // fecha só o backend da worksheet e seu
Break; // substream de inflate; 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; // ainda nosso, ainda válido, posição restaurada
end;
end;
Emprestando a linha atual sem copiá-la
O IXLSRowCursorView entrega uma linha a outra rotina sem duplicar o array de células. A view armazena um guard compartilhado contendo o ponteiro do cursor mais um contador de geração UInt64; avançar, selecionar uma sheet, cancelar, fechar e destruir o cursor todos incrementam essa geração, e a destruição adicionalmente limpa o dono do guard. Assim uma view obsoleta não pode ler memória liberada: Valid é uma sonda sem exceção que você pode chamar a qualquer momento, enquanto todo outro membro valida primeiro e levanta EXLSRowCursorViewInvalidated. Seja honesto quanto ao que este contrato é — é fail-fast de lifetime, não uma garantia de thread-safety, e ele não licencia ler uma linha de uma segunda thread enquanto a primeira avança o cursor
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // empresta; nenhum array de células é copiado
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // leituras BIFF forward mantêm o
else if Cell.Kind = xdkEmpty then // resultado em cache, não os tokens
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank são células reais
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// A interface sobrevive ao loop, mas a linha por trás dela não
if not View.Valid then // Valid nunca levanta; Cells[] agora levantaria
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes, e o que ele tem permissão de provar
O PeakRowBufferedBytes existe para demonstrar que a memória acompanha a largura da linha em vez da contagem de linhas. Ele acumula os records de célula, Variants, strings de fórmula e payloads de rich-text da linha de saída atual e incorpora o working set específico do formato — o record lógico do CSV, o template de linha física do ODS, o pico de records do BIFF, ou a célula bruta do XLSX em decodificação no momento. Leia-o junto com SheetPassesStarted, que conta quantos passes de worksheet realmente começaram. Duas ressalvas mantêm isso honesto: o número é uma estimativa, não uma contabilidade exata de heap, e ele é monotônico desde o Open mais recente, então é um instrumento de depuração e regressão em vez de um medidor ao vivo. Para o quadro mais amplo de para onde vão tempo e bytes em livros muito grandes, veja performance de pastas de trabalho grandes em Delphi
O leitor push virou um adaptador, e o que o cursor não fará
O TXLSForwardReader não carrega mais pontos de entrada de varredura separados para XLSX, BIFF e texto. Ele configura um cursor, o percorre, e traduz a linha atual em eventos OnSheet e OnCell, que é por que as duas fachadas não podem mais divergir em filtragem, estado de fórmula ou tratamento de erro. Duas consequências valem conhecer antes de atualizar: o SheetIndex do callback agora é uniformemente base 1 no TXLSForwardReader (o TXLSDirectReader mantém seu contrato de eventos base 0 existente), e OnSheet dispara antes de SelectSheet, então definir SkipSheet significa que a worksheet part nunca é aberta nem descomprimida. Os limites são igualmente explícitos: a pasta de trabalho não deve ser modificada enquanto um passe está ativo, cancelar exige um reinício explícito, e o caminho BIFF forward nunca decompila tokens de fórmula, então células de fórmula clássica relatam HasFormula true com FormulaTextAvailable false e entregam a você o resultado em cache em vez de inventar uma string de fórmula vazia. O cursor de linhas e seu adaptador passaram em 1.298 verificações no Delphi Win32 e Win64 mais o pacote estático Win64 do C++Builder 37.0
Se você está ponderando um cursor pull contra o loader que tem agora, a pergunta a fazer não é qual analisa mais rápido, mas qual permite escrever a condição de saída de que você realmente precisa. Detalhes completos do componente, versões de IDE suportadas e licenciamento estão na página do componente de planilha Delphi HotXLS