Artigo Técnico

HotXLS: sheet listing and lightweight workbook inspection

Por vezes, a única pergunta que uma rotina de triagem de entrada necessita de ver respondida é estrutural: se este livro tem uma folha chamada 'Mapping', ou quantos separadores possui. Responder a isso chamando Open é a via mais dispendiosa. Uma abertura completa inflaciona a tabela de strings partilhadas (shared string table), descodifica cada registro de estilo e percorre as células de cada folha de cálculo, porque não tem forma de saber que apenas pretendia o índice. Num arquivo de grandes dimensões, isto traduz-se em centenas de megabytes de alocações e vários segundos de processador gastos para ler uma lista que ocupa escassos kilobytes. O HotXLS, a biblioteca nativa de folhas de cálculo para Delphi da losLab, fornece essa lista de forma isolada: o método GetSheetNames devolve os nomes das folhas de cálculo, na ordem do livro, sem instanciar uma única célula

Por que razão o catálogo é barato de ler

Ambos os formatos de folha de cálculo colocam o seu índice perto do início do arquivo, o que torna a chamada de listagem rápida e não propriamente complexa. Um pacote OOXML mantém o catálogo de folhas em xl/workbook.xml, uma secção que se mantém pequena quer o livro tenha dez linhas ou dez milhões. Um arquivo BIFF8 .xls armazena os seus registros BoundSheet no início do fluxo global do livro, antes de quaisquer dados de células. Assim, o trabalho que uma chamada de listagem evita não é um mero arredondamento face a uma abertura completa. Evita a maior parte do arquivo. Ler o catálogo custa o mesmo punhado de kilobytes independentemente do número de linhas, enquanto uma abertura completa escala com os dados; num livro de cálculo de vários megabytes, essa diferença atinge várias ordens de grandeza em bytes processados e memória alocada

Esse custo fixo é a propriedade em torno da qual vale a pena desenhar o sistema. Um portal de triagem construído com GetSheetNames comporta-se da mesma forma num arquivo de 200 linhas ou num de 200 MB, pelo que o arquivo mais lento de um lote deixa de ditar o ritmo para decidir se vale a pena processar o arquivo

Uma única chamada para .xls, .xlsx e formatos de modelos

Na fachada XLS, o TXLSWorkbook.GetSheetNames lê mais do que apenas arquivos .xls. Também aceita os formatos baseados em zip .xlsx, .xlsm, .xltx e .xltm, extraindo apenas o workbook.xml do arquivo. Para inputs .xls genuínos, analisa os registros BoundSheet e para no primeiro registro EOF do subfluxo global, pelo que um arquivo binário grande continua a custar apenas os seus kilobytes iniciais. A fachada XLSX oferece uma garantia que importa mais para código de serviço de longa duração do que parece à primeira vista: o TXLSXWorkbook.GetSheetNames deixa a instância do livro nem limpa nem preenchida, pelo que uma instância que já contenha um documento aberto pode sondar outros arquivos sem perturbar o que está em mãos. O GetODSSheetNames aplica a mesma abordagem a pacotes OpenDocument, e cada uma destas chamadas tem uma sobrecarga de fluxo (stream), o que lhe permite inspecionar um upload que nunca chega a ser gravado no disco

A mesma chamada serve para um bom diálogo de importação em aplicações desktop. Liste as folhas, permita que o usuário escolha uma e pague pela abertura total apenas após a seleção ter sido feita. Num livro de cálculo com cinquenta folhas, a diferença é visível: um selecionador que aparece imediatamente contra um que bloqueia enquanto o arquivo completo é carregado em segundo plano

Ficheiros .xlsm com macros ativas e os formatos de modelos são listados exatamente como um .xlsx simples, uma vez que o catálogo reside no mesmo workbook.xml quer haja ou não um vbaProject.bin dentro do pacote. Um pipeline de entrada pode, por isso, enumerar as folhas de um livro com macros para fins de encaminhamento, sem nunca tocar no payload das macros e sem fazer nada que as pudesse executar, deixando a decisão sobre a política de macros para a etapa que efetivamente abre o arquivo

var
  Book: TXLSXWorkbook;
  Names: TStringList;
  I: Integer;
begin
  Names := TStringList.Create;
  Book := TXLSXWorkbook.Create;
  try
    if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
      raise Exception.Create('unreadable workbook package');
    if Names.IndexOf('Mapping') < 0 then
      raise Exception.Create('required Mapping sheet is missing');
    for I := 0 to Names.Count - 1 do
      Writeln(Format('sheet %d: %s', [I, Names[I]]));
  finally
    Book.Free;
    Names.Free;
  end;
end;

Ler o valor de retorno sem enganos

As convenções de retorno não são uniformes no HotXLS. Algumas chamadas devolvem 1 em caso de sucesso, outras devolvem uma contagem, pelo que, para as funções de listagem, a única validação que se sustenta é tratar qualquer valor igual ou inferior a zero como falha, limpando a lista de strings. Resista à tentação de interpretar uma lista vazia como 'um livro sem folhas'. Tanto a norma ECMA-376 como a especificação BIFF8 exigem pelo menos uma folha de cálculo num livro válido, pelo que zero nomes significa sempre que a leitura falhou, e nunca que o arquivo está legitimamente vazio

Uma listagem falhada é, por si só, um sinal que vale a pena registar. Um arquivo .xlsx que falhe na chamada indica uma de várias situações específicas: está truncado, não é realmente um pacote OOXML (exportações CSV mal etiquetadas de outros sistemas surgem constantemente aqui) ou é um contentor encriptado. Distinguir estas situações é a tarefa da verificação seguinte. Registar os primeiros bytes do arquivo rejeitado juntamente com o log de falha costuma encurtar a resolução de um pedido de suporte para uma única mensagem

Detetar contentores encriptados antes do encaminhamento

Um arquivo .xlsx encriptado não é um zip. É um arquivo composto OLE que envolve os fluxos EncryptionInfo e EncryptedPackage, pelo que o GetSheetNames não o consegue ler por dentro e devolve falha como qualquer outro arquivo ilegível. O CanReadEncrypted testa essa estrutura de contentor, permitindo que a triagem encaminhe um arquivo encriptado de propósito, em vez de ocultar um erro de leitura genérico vindo das profundezas de um processo de trabalho:

A encriptação é o ponto onde o HotXLS é deliberadamente assimétrico, pelo que o encaminhamento tem de respeitar isso. A encriptação legada .xls (RC4, RC4 CryptoAPI, XOR) é legível: TXLSWorkbook.Open(FileName, Password) desencripta com uma palavra-passe armazenada, pelo que esses arquivos podem permanecer no caminho automatizado. Já os pacotes OOXML encriptados seguem a via oposta. O HotXLS consegue escrever um com SaveAsEncrypted, mas não o consegue ler de volta. O método OpenEncrypted gera a exceção EXlsxEncryptionNotImplemented quando se lhe passa um pacote encriptado, razão pela qual um design de triagem correto envia arquivos .xlsx encriptados para um usuário humano com o Excel e mantém os arquivos .xls com palavra-passe no código

Para trabalhos em lote, este classificador justifica a sua utilização ao correr sobre um diretório de entrada inteiro antes de qualquer processo iniciar o processamento real, uma vez que cada verificação custa cerca de uma abertura de arquivo e alguns kilobytes de leituras. Colocar este passo logo no início altera o modo de falha com que a equipa de operações se preocupa. Em vez de uma tarefa noturna falhar às 3 da manhã no arquivo 412 de 600, obtém 412 arquivos em fila e 5 rejeitados na entrada com um motivo associado a cada um. As mesmas chamadas de biblioteca resultam numa história operacional muito melhor

type
  TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);

function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    // Encrypted OOXML is an OLE container, not a zip: check first,
    // because the listing calls cannot look inside it.
    if Book.CanReadEncrypted(FileName) then
      Exit(irNeedsPassword);
    if SameText(ExtractFileExt(FileName), '.ods') then
    begin
      if Book.GetODSSheetNames(FileName, Names) <= 0 then
        Exit(irUnreadable);
    end
    else if Book.GetSheetNames(FileName, Names) <= 0 then
      Exit(irUnreadable);
    Result := irNormal;
  finally
    Book.Free;
  end;
end;

As perguntas que uma chamada de listagem não pode responder

Nomes e ordem são tudo o que obtém. Las chamadas de listagem nada dizem sobre visibilidade, pelo que folhas ocultas e muito ocultas (very-hidden) aparecem na lista como qualquer outra. Não reportam dimensões de intervalo utilizado, contagem de células ou propriedades do documento. A secção docProps/core.xml também é pequena, mas não existe atualmente uma verificação apenas de propriedades, pelo que os metadados de autor e título continuam a custar uma abertura Open completa. A forma limpa de gerir isto é deixar que os fatos baratos encaminhem cada arquivo e reservar os dados dispendiosos para os arquivos que superam a triagem. Para os arquivos que prosseguem para uma leitura aprofundada, um varrimento de apenas leitura de um arquivo .xls grande corre significativamente mais rápido com _DisableGraphics := True, o que ignora a análise de OfficeArt. Contudo, nunca guarde a partir dessa instância: a camada gráfica ignorada desaparece do modelo, e guardar descartaria os desenhos do arquivo

Os arquivos que passam na triagem costumam avançar para uma análise mais profunda. O artigo sobre auditoria de livros e bancada de conversão cobre os contadores por folha que vale a pena recolher quando se justifica uma abertura total, e o guia de desempenho para grandes livros de cálculo ensina a manter essa abertura completa rápida

O HotXLS é uma biblioteca nativa de folhas de cálculo em Object Pascal para Delphi e C++Builder; a totalidade da API, incluindo as chamadas de inspeção apresentadas aqui, está documentada na página do Componente HotXLS