Imagine um serviço Delphi noturno encarregue de gerar um XLSX por cliente (algumas centenas de ficheiros, alguns com 400.000 linhas). Ao realizar o profiling do processo, o ponto crítico raramente é o ciclo de preenchimento das células, mas sim a chamada a SaveAs. Com o gerador padrão, cada folha é serializada numa única string XML em memória antes de ser comprimida no arquivo zip OOXML; em folhas volumosas, esta string temporária ultrapassa o tamanho do próprio modelo de células. Como tal, uma tarefa que processa confortavelmente os dados consumindo 800 MB registará um pico superior ao limite de 2 GB de memória durante a gravação, provocando falhas por falta de memória (OOM) a meio da noite. O HotXLS, a biblioteca nativa Delphi da losLab para folhas de cálculo, dispõe de uma propriedade concebida especificamente para este cenário: StreamingWrite. Associados a esta, existem dois controlos adicionais que determinam se o processamento em lote se mantém dentro do orçamento de tempo e memória: os callbacks de escrita ao nível da linha e a otimização do pool de estilos em ciclos iterativos
O que o fluxo padrão armazena e o que o StreamingWrite altera
O gerador XLSX padrão prioriza a simplicidade: processa o XML da folha por comportamento completo e envia a string resultante para o compressor zip. Esta abordagem é correta para a maioria dos livros de cálculo, onde o XML de cada folha ocupa poucos megabytes. Contudo, falha quando a versão serializada atinge centenas de megabytes. O XML de folhas de cálculo é extenso (cada célula numérica consome dezenas de caracteres de markup e a string que contém a folha completa tem de ser contígua). O gráfico de consumo de memória exibe um comportamento claro: uma linha plana longa durante o preenchimento das linhas, um pico triangular acentuado na chamada a SaveAs e a subsequente libertação de memória após a escrita do zip
Definir Book.StreamingWrite := True altera o comportamento de SaveAs para um gerador que escreve o XML da folha diretamente no fluxo do zip à medida que este é produzido. A string intermédia deixa de ser alocada e o pico de consumo é eliminado
Seja rigoroso sobre o benefício real: este parâmetro altera apenas o fluxo de gravação. A construção do livro continua a alocar o modelo completo na memória; logo, o patamar de consumo durante a fase de preenchimento mantém-se idêntico. O que é eliminado é o pico de serialização que se sobrepunha a esse patamar no momento da gravação. Em tarefas de 400.000 linhas, este pico dita a diferença entre respeitar os limites de memória ou provocar falhas no sistema. A propriedade tem o valor padrão False por compatibilidade histórica; como tal, a sua ativação requer uma linha de código explícita
Uma exportação em lote com o parâmetro ativo
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // índice do pool, baseado em 0
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
if (R mod 1000) = 0 then
Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // baseado em 1 na célula
end;
Book.StreamingWrite := True; // o XML da folha é transmitido por streaming para o contentor zip
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
O acesso por Cells[R, C] cria as células a pedido, simplificando a escrita do ciclo. Há dois limites de grelha importantes a reter: 1.048.576 linhas e 16.384 colunas, expostos nas constantes XlsxMaxRow and XlsxMaxCol. Se o volume de dados exceder o limite de linhas, o seu código deve dividi-los por várias folhas. O motor de escrita não trata este excesso de forma automática, resultando na gravação de um ficheiro truncado
Preencher linhas sem o overhead de Variant por célula
Cada atribuição a Cells[R, C].Value implica uma pesquisa de célula e uma conversão de Variant. Em tabelas de dez mil linhas, este impacto é irrelevante. Numa tabela de um milhão de linhas com vinte colunas, o overhead acumulado passa a dominar o tempo de execução e o profiler identificará a chamada como gargalo. As interfaces em lote permitem-lhe fornecer uma linha completa de cada vez ao motor. O método WriteRows aciona um callback que fornece uma linha por chamada:
procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
LastCol: Integer; var Values: Variant; var Skip: Boolean;
var Cancel: Boolean);
begin
if not FReader.Next then
begin
Cancel := True; // a origem de dados está vazia: parar de forma limpa
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// preencher as linhas 2..100001, colunas A..C, lendo do leitor
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
O marcador Cancel permite converter um intervalo fixo num formato "até N linhas", útil quando o volume total decorre de uma consulta em execução. O marcador Skip tem impacto menor, mantendo uma linha em branco sem interromper a execução. Além de preencher células, o callback configura um local adequado para centralizar tarefas operacionais (como contadores de progresso a cada mil linhas, verificação de sinais de cancelamento da tarefa ou controlo de ritmo de leitura da base de dados). Toda esta lógica passa a residir num único local em vez de ser dispersa pelo código de escrita. Na leitura de dados, os métodos ForEachRow e ForEachCell replicam o mesmo padrão, relevante se a tarefa consumir e produzir ficheiros volumosos
O pool de estilos beneficia da declaração externa (hoisting)
O modelo de formatação XLSX baseia-se em pools partilhados. Os métodos Fonts.Add, Fills.AddSolid e Borders.Add devolvem um índice baseado em 0, e a célula referencia o tipo de letra guardando esse índice incrementado em uma unidade em FontIndex (onde o valor zero está reservado para a formatação padrão). A soma + 1 está exemplificada no código acima: se a omitir, a célula assumirá um estilo incorreto, pois um índice desfasado continua a ser um índice válido no pool e não gera exceções
A boa prática consiste em instanciar os objetos de estilo fora do ciclo iterativo e referenciar os índices resultantes. O método Fonts.Add consolida definições duplicadas automaticamente, pelo que chamadas consecutivas apenas consomem ciclos de CPU. Contudo, Alignments.Add constitui uma armadilha porque devolve um novo objeto a cada chamada. Num ciclo de 100.000 iterações, isto gerará cem mil registos de alinhamento duplicados no ficheiro styles.xml, aumentando o tamanho do documento e atrasando o carregamento no Excel. Instancie cada estilo uma vez fora do ciclo e reutilize o índice obtido
Fluxos, pastas temporárias e a gestão do ciclo de processamento
Nenhuma destas operações exige o sistema de ficheiros. Ambas as interfaces disponibilizam sobrecargas para o tipo TStream em todos os métodos de entrada e saída (como Open, SaveAs, SaveAsCSV, SaveAsHTML e SaveAsODS); isto permite a um processo de servidor gerar dados num TMemoryStream destinado a armazenamento de objetos (blob storage) ou respostas HTTP sem passar pelo disco. Há uma salvaguarda importante: o método SaveAs(Stream) escreve a partir da posição atual do fluxo e não o reposiciona no final (garanta a instrução Position := 0 antes de entregar o fluxo para leitura). A interface XLS disponibiliza dois controlos específicos: SetTempDir direciona os ficheiros temporários do gerador BIFF para uma unidade com espaço e desempenho de E/S adequados (relevante em servidores onde a pasta temporária padrão resida num disco de sistema congestionado); e UseSharedFormulas agrupa corpos de fórmulas repetidos em conjuntos partilhados, reduzindo substancialmente o tamanho do documento em relatórios onde uma fórmula se repete ao longo de toda a coluna
O ciclo de processamento mantém-se simples por definição:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // nova instância: sem contaminação de estado
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // um ficheiro corrompido não deve interromper o lote
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
Instanciar um novo livro por ficheiro consome microssegundos e evita contaminações de estado entre documentos (como estilos, nomes definidos e propriedades a passarem de um ficheiro para o seguinte). O tratamento de erros com continuação perante falhas em Open é igualmente crítico: uma falha num ficheiro a meio de um lote de 600 documentos deve resultar apenas num registo de log e não na paragem de todo o processo. Lembre-se também de que o método SaveAsCSV não calcula fórmulas, exportando-as como texto literal. Se o sistema que consome o CSV exigir os valores calculados, execute o método Calculate nas células respetivas antes da exportação, ou garanta que o livro de origem já contém os valores em cache
Modelo de concorrência: um livro de cálculo por thread
Os objetos das duas interfaces não são seguros para partilha entre threads (thread-safe). Como não existe estado global partilhado entre instâncias, a escala é linear: atribua um livro por thread de trabalho e nunca partilhe o mesmo objeto entre threads distintas. Um pool de N processos, gerindo cada um a sua própria instância TXLSXWorkbook, escala quase linearmente até atingir os limites de memória (limite este equivalente ao tamanho do maior modelo de células multiplicado pelo número de processos simultâneos, somado à poupança proporcionada pelo método StreamingWrite). Se a fila de tarefas crescer, faça a gestão de recursos na fila de espera e não no motor de escrita: uma thread sem recursos que grave apenas metade de um livro não produz resultados úteis, ao passo que uma tarefa que aguarde alguns segundos por um processo disponível concluirá com sucesso
Para uma visão abrangente sobre otimização (incluindo fórmulas partilhadas, omissão de gráficos na leitura e os controlos da faceta XLS), consulte o guia de desempenho para grandes livros de cálculo. Processamentos em lote com origem em consultas de base de dados são abordados separadamente no artigo sobre exportação de base de dados para Delphi
O HotXLS é uma biblioteca nativa Object Pascal de folhas de cálculo para Delphi e C++Builder; a referência completa da API de proteção e configuração de página está disponível na página do produto HotXLS Component