Peça a um pipeline para rotear dez mil planilhas por autor, empresa ou data de última modificação, e a pior coisa que ele pode fazer é abrir cada pasta de trabalho por completo. As respostas viajam nas propriedades de documento do arquivo, o que o mundo do Office chama de Informações de Resumo do Documento: a camada de metadados que o Windows Search indexa, pela qual o SharePoint arquiva e que o Excel exibe em sua caixa de diálogo Propriedades. Essa camada tem, no máximo, alguns kilobytes, e vive em um lugar bem documentado em ambos os formatos do Excel. O truque é alcançá-la a partir do Delphi sem pagar pelo milhão de células de que você não precisa
Existem três rotas reais, e elas diferem menos no que retornam do que naquilo que exigem da máquina que as executa. A automação COM aciona o próprio Excel e lê tudo, a preço de desktop. O formato .xls mantém suas propriedades em fluxos de conjunto de propriedades OLE que o Windows analisa para você. O formato .xlsx as mantém em duas pequenas partes XML dentro de um zip que a RTL do Delphi consegue abrir sozinha. Segue o código funcional de cada uma, com os custos declarados sem rodeios
Rota 1: a automação COM lê tudo, a preço de desktop
A automação é a única rota com cobertura total através de um único modelo de objetos: o conjunto de resumo padrão, o conjunto estendido com Company e Manager, e as propriedades personalizadas definidas pelo usuário, todos acessíveis por meio de BuiltinDocumentProperties e CustomDocumentProperties. Tudo chega como um OleVariant, e a API tem um hábito que vale a pena conhecer antes que ele te morda: uma propriedade interna que nunca foi atribuída não volta vazia, ela lança uma EOleException no instante em que você toca em Value. O auxiliar abaixo trata isso como "não definida" em vez de como uma falha
uses
System.SysUtils, System.Variants, System.Win.ComObj;
procedure ReadPropertiesViaCom(const FileName: string);
var
Excel, Book, Builtin, Custom: OleVariant;
I: Integer;
function BuiltinProp(const Name: string): string;
begin
try
Result := VarToStr(Builtin.Item(Name).Value);
except
on EOleError do
Result := ''; // a propriedade existe, mas nunca foi atribuída
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // somente leitura
try
Builtin := Book.BuiltinDocumentProperties;
Writeln('Author : ', BuiltinProp('Author'));
Writeln('Title : ', BuiltinProp('Title'));
Writeln('Subject: ', BuiltinProp('Subject'));
Writeln('Company: ', BuiltinProp('Company'));
Writeln('Manager: ', BuiltinProp('Manager'));
Custom := Book.CustomDocumentProperties;
for I := 1 to Custom.Count do
Writeln(VarToStr(Custom.Item(I).Name), ' = ',
VarToStr(Custom.Item(I).Value));
finally
Book.Close(False);
end;
finally
Excel.Quit; // alcance isto em todos os caminhos, ou o EXCEL.EXE fica para trás
Excel := Unassigned;
end;
end;
Agora a conta. O Excel precisa estar instalado em cada máquina em que este código roda, o que já descarta a maioria dos servidores por si só, e a política de suporte da Microsoft é explícita ao afirmar que o Office não é projetado nem licenciado para automação server-side desassistida. O CreateOleObject inicia um EXCEL.EXE completo e o Workbooks.Open analisa a pasta de trabalho inteira, então espere algo em torno de dois a quatro segundos por arquivo antes que a primeira propriedade retorne. E o try..finally em volta do Quit não é enfeite: uma exceção que escape entre o CreateOleObject e o Quit deixa um EXCEL.EXE órfão segurando um lock sobre o arquivo, invisível até que a próxima execução falhe contra ele. Reutilizar uma única instância do Excel ao longo de um lote amortiza o custo de inicialização, mas concentra o risco, porque uma única caixa de diálogo perdida no desktop oculto trava todos os arquivos enfileirados atrás dela
Rota 2: o .xls armazena propriedades em fluxos de conjunto de propriedades OLE
Uma pasta de trabalho BIFF8 é um arquivo composto OLE, um sistema de arquivos em miniatura de armazenamentos (storages) e fluxos (streams). Os dados das células vivem no fluxo Workbook; os metadados vivem ao lado, em dois fluxos de conjunto de propriedades cujos nomes começam com o caractere de controle #5: \005SummaryInformation para os campos clássicos e \005DocumentSummaryInformation para os estendidos e personalizados. Dentro de cada um há um conjunto de propriedades binário no layout MS-OLEPS, com seções indexadas por um identificador de formato (FMTID) e propriedades indexadas por um ID de propriedade inteiro. A seção de resumo é o FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, onde PIDSI_TITLE é $02 e PIDSI_AUTHOR é $04; Company ($0F) e Manager ($0E) vivem na seção de resumo do documento, e as propriedades personalizadas em uma segunda seção atrás de um dicionário de nomes
A boa notícia é que no Windows você nunca analisa esses bytes você mesmo. O armazenamento estruturado expõe os fluxos por meio de IPropertySetStorage, e o código a seguir compila tal como mostrado contra as unidades de fábrica da RTL
uses
System.SysUtils, Winapi.Windows, Winapi.ActiveX, System.Win.ComObj;
const
FMTID_SummaryInfo: TGUID = '{F29F85E0-4FF9-1068-AB91-08002B27B3D9}';
PIDSI_TITLE = $02;
PIDSI_AUTHOR = $04;
STGFMT_STORAGE = 0;
function ReadXlsSummaryString(const FileName: string; PropId: TPropID): string;
var
Unk: IUnknown;
Stg: IStorage;
PropSetStg: IPropertySetStorage;
PropStg: IPropertyStorage;
Spec: TPropSpec;
Value: TPropVariant;
begin
Result := '';
OleCheck(StgOpenStorageEx(PWideChar(FileName),
STGM_READ or STGM_SHARE_DENY_WRITE, STGFMT_STORAGE, 0, nil, nil,
@IID_IStorage, Unk));
Stg := Unk as IStorage;
PropSetStg := Stg as IPropertySetStorage;
OleCheck(PropSetStg.Open(FMTID_SummaryInfo,
STGM_READ or STGM_SHARE_EXCLUSIVE, PropStg));
Spec.ulKind := PRSPEC_PROPID;
Spec.propid := PropId;
if PropStg.ReadMultiple(1, @Spec, @Value) = S_OK then // S_FALSE: ausente
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// uso: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Uma palavra honesta sobre o que o trecho esconde. As strings podem chegar como VT_LPWSTR ou como VT_LPSTR, e no caso ANSI os bytes são codificados na própria code page do conjunto de propriedades, ela mesma armazenada como propriedade 1 da seção, de modo que o cast acima só é exato quando essa code page coincide com a do sistema. Os carimbos de data e hora voltam como VT_FILETIME em UTC. As propriedades personalizadas significam abrir a seção definida pelo usuário, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, e percorrer seu dicionário de nomes. O IPropertyStorage absorve tudo isso no Windows; escrever seu próprio parser MS-OLEPS para um ambiente sem armazenamento estruturado é um projeto de verdade, não uma tarde de trabalho
Rota 3: o .xlsx mantém docProps como XML dentro do zip
Esta é a rota de que a maioria dos pipelines realmente precisa, já que os arquivos novos vêm em .xlsx há quase duas décadas. Uma pasta de trabalho OOXML é um pacote zip, e suas propriedades são divididas em pequenas partes por finalidade: docProps/core.xml contém os campos Dublin Core, dc:title, dc:creator, cp:lastModifiedBy, além de dcterms:created e dcterms:modified como carimbos de data e hora W3CDTF em UTC, enquanto docProps/app.xml contém campos de nível de aplicativo, como Company e AppVersion, e docProps/custom.xml contém as propriedades personalizadas. Como o diretório central do zip localiza cada parte diretamente, lê-las custa alguns poucos kilobytes, não importa o tamanho da pasta de trabalho. TZipFile e IXMLDocument, ambos na RTL entregue, fazem o trabalho todo
uses
System.SysUtils, System.Classes, System.Zip, Xml.XMLDoc, Xml.XMLIntf;
const
NsDC = 'http://purl.org/dc/elements/1.1/';
NsTerms = 'http://purl.org/dc/terms/';
NsCore = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties';
NsApp = 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties';
function PartToXml(Zip: TZipFile; const PartName: string): IXMLDocument;
var
Bytes: TBytes;
begin
Zip.Read(PartName, Bytes);
Result := LoadXMLData(TEncoding.UTF8.GetString(Bytes));
end;
function Field(const Doc: IXMLDocument; const LocalName, Ns: string): string;
var
Node: IXMLNode;
begin
Node := Doc.DocumentElement.ChildNodes.FindNode(LocalName, Ns);
if Node <> nil then
Result := Node.Text
else
Result := '';
end;
procedure ReadXlsxProperties(const FileName: string);
var
Zip: TZipFile;
Doc: IXMLDocument;
begin
Zip := TZipFile.Create;
try
Zip.Open(FileName, zmRead);
if Zip.IndexOf('docProps/core.xml') >= 0 then
begin
Doc := PartToXml(Zip, 'docProps/core.xml');
Writeln('Title : ', Field(Doc, 'title', NsDC));
Writeln('Creator : ', Field(Doc, 'creator', NsDC));
Writeln('Modifier: ', Field(Doc, 'lastModifiedBy', NsCore));
Writeln('Modified: ', Field(Doc, 'modified', NsTerms)); // W3CDTF, UTC
end;
if Zip.IndexOf('docProps/app.xml') >= 0 then
begin
Doc := PartToXml(Zip, 'docProps/app.xml');
Writeln('Company : ', Field(Doc, 'Company', NsApp));
Writeln('App : ', Field(Doc, 'Application', NsApp), ' ',
Field(Doc, 'AppVersion', NsApp));
end;
finally
Zip.Free;
end;
end;
Dois detalhes mantêm isto robusto em produção. Primeiro, as partes são opcionais: um pacote mínimo sem nenhum docProps é perfeitamente válido sob a ECMA-376, e é por isso que o código sonda com IndexOf em vez de presumir. Segundo, faça a correspondência dos elementos por nome local e URI de namespace, como o FindNode faz acima, nunca por prefixo literal; dc: e cp: são convenções do gerador de escrita do Excel, e arquivos produzidos por outros geradores têm liberdade para escolher prefixos diferentes. Uma observação ambiental: o fornecedor padrão do IXMLDocument é o MSXML, portanto uma aplicação de console ou uma thread de trabalho precisa chamar CoInitialize antes de LoadXMLData, ou a primeira análise morre com um erro COM
A planilha de custos, e quando uma biblioteca supera ambos os parsers
Medida em uma máquina de desenvolvedor comum, a rota COM fica em torno de dois a quatro segundos por arquivo quando a sessão de automação é criada por arquivo, quase tudo isso sendo a inicialização do EXCEL.EXE mais a análise completa da pasta de trabalho, e ela exige um Excel instalado e licenciado onde quer que rode. As duas rotas diretas leem apenas os contêineres de metadados, terminam em milissegundos de um só dígito por arquivo e não precisam de nada instalado além do que um executável Delphi já linka. Ao longo de um compartilhamento de dez mil arquivos, essa é a diferença entre a maior parte de um dia de trabalho e menos de um minuto, sem nenhuma questão de implantação do Office anexada
O problema com as rotas diretas é que existem duas delas. Um pipeline que aceita ambos os formatos mantém dois parsers com dois modos de falha disjuntos, code pages e tipos PROPVARIANT de um lado, namespaces e partes opcionais do outro, e nenhum lê o formato do outro. Essa carga de manutenção é o argumento a favor de uma biblioteca nativa: o HotXLS, a biblioteca de planilhas em Object Pascal da losLab para Delphi e C++Builder no Windows, expõe os mesmos campos como propriedades comuns da pasta de trabalho, Title, Author, Company, Created e o resto, populados pelo Open tanto para .xls quanto para .xlsx, sem instalação do Excel e sem nada do encanamento de contêiner acima. Ele lê as propriedades como parte de uma abertura completa da pasta de trabalho, em vez de uma sondagem apenas de metadados, então se encaixa em pipelines que vão em seguida acessar os dados das células de qualquer forma; toda a superfície de propriedades em ambas as fachadas, incluindo o lado de escrita, é coberta em nosso artigo sobre como definir propriedades de documento do Excel com HotXLS
Nota: Ferramentas completas de análise do Excel e extração de metadados estão disponíveis no HotXLS VCL Component