Pídale a un pipeline que enrute diez mil hojas de cálculo por autor, empresa o fecha de última modificación, y lo peor que puede hacer es abrir por completo cada libro. Las respuestas viajan en las propiedades del documento del archivo, lo que el mundo Office llama Document Summary Information: la capa de metadatos que Windows Search indexa, por la que SharePoint clasifica y que Excel muestra en su diálogo de Propiedades. Esa capa pesa kilobytes como mucho, y vive en un lugar bien documentado en ambos formatos de Excel. El truco está en llegar a ella desde Delphi sin pagar por el millón de celdas que usted no necesita
Hay tres rutas reales, y difieren menos en lo que devuelven que en lo que exigen de la máquina que las ejecuta. La automatización COM maneja el propio Excel y lee todo, a precio de escritorio. El formato .xls guarda sus propiedades en flujos de conjuntos de propiedades OLE que Windows analizará por usted. El formato .xlsx las guarda en dos pequeñas partes XML dentro de un zip que la RTL de Delphi puede abrir por sí sola. A continuación hay código funcional para cada una, con los costos expuestos con claridad
Ruta 1: la automatización COM lee todo, a precio de escritorio
La automatización es la única ruta con cobertura total a través de un solo modelo de objetos: el conjunto de resumen estándar, el conjunto extendido con Company y Manager, y las propiedades personalizadas definidas por el usuario, todas accesibles mediante BuiltinDocumentProperties y CustomDocumentProperties. Todo llega como un OleVariant, y la API tiene un hábito que conviene conocer antes de que muerda: una propiedad integrada que nunca fue asignada no regresa vacía, sino que lanza una EOleException en el momento en que usted toca Value. El asistente de abajo trata eso como "no establecida" en lugar de como una falla
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 := ''; // la propiedad existe pero nunca fue asignada
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // solo lectura
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; // llegue aquí en toda ruta, o EXCEL.EXE queda atrás
Excel := Unassigned;
end;
end;
Ahora la cuenta. Excel debe estar instalado en cada máquina donde corre este código, lo que por sí solo descarta la mayoría de los servidores, y la política de soporte de Microsoft es explícita en que Office no está diseñado ni licenciado para automatización desatendida del lado del servidor. CreateOleObject lanza un EXCEL.EXE completo y Workbooks.Open analiza todo el libro, así que espere aproximadamente de dos a cuatro segundos por archivo antes de que regrese la primera propiedad. Y el try..finally alrededor de Quit no es decoración: una excepción que escapa entre CreateOleObject y Quit deja un EXCEL.EXE huérfano sosteniendo un bloqueo sobre el archivo, invisible hasta que la siguiente ejecución falla contra él. Reutilizar una instancia de Excel a lo largo de un lote amortiza el costo de arranque pero concentra el riesgo, porque un solo diálogo perdido en el escritorio oculto detiene cada archivo en cola detrás de él
Ruta 2: .xls guarda las propiedades en flujos de conjuntos de propiedades OLE
Un libro BIFF8 es un archivo compuesto OLE, un sistema de archivos en miniatura de almacenamientos y flujos. Los datos de las celdas viven en el flujo Workbook; los metadatos viven a su lado en dos flujos de conjuntos de propiedades cuyos nombres comienzan con el carácter de control #5: \005SummaryInformation para los campos clásicos y \005DocumentSummaryInformation para los extendidos y personalizados. Dentro de cada uno hay un conjunto de propiedades binario con el diseño MS-OLEPS, con secciones indexadas por un identificador de formato (FMTID) y propiedades indexadas por un ID de propiedad entero. La sección de resumen es el FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, donde PIDSI_TITLE es $02 y PIDSI_AUTHOR es $04; Company ($0F) y Manager ($0E) viven en la sección de resumen del documento, y las propiedades personalizadas en una segunda sección detrás de un diccionario de nombres
La buena noticia es que en Windows usted nunca analiza esos bytes por su cuenta. El almacenamiento estructurado expone los flujos a través de IPropertySetStorage, y lo siguiente compila tal como se muestra contra las unidades estándar de la 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: no presente
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));
Una palabra honesta sobre lo que el fragmento oculta. Las cadenas pueden llegar como VT_LPWSTR o como VT_LPSTR, y en el caso ANSI los bytes están codificados en la página de códigos propia del conjunto de propiedades, almacenada a su vez como la propiedad 1 de la sección, así que la conversión de arriba solo es exacta cuando esa página de códigos coincide con la del sistema. Las marcas de tiempo regresan como VT_FILETIME en UTC. Las propiedades personalizadas implican abrir la sección definida por el usuario, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, y recorrer su diccionario de nombres. IPropertyStorage absorbe todo eso en Windows; escribir su propio analizador MS-OLEPS para un entorno sin almacenamiento estructurado es un proyecto genuino, no una tarde
Ruta 3: .xlsx guarda docProps como XML dentro del zip
Esta es la ruta que la mayoría de los pipelines realmente necesita, ya que los archivos nuevos han sido .xlsx durante casi dos décadas. Un libro OOXML es un paquete zip, y sus propiedades están repartidas en partes pequeñas según su propósito: docProps/core.xml contiene los campos Dublin Core, dc:title, dc:creator, cp:lastModifiedBy, más dcterms:created y dcterms:modified como marcas de tiempo W3CDTF en UTC, mientras que docProps/app.xml contiene campos a nivel de aplicación como Company y AppVersion, y docProps/custom.xml contiene las propiedades personalizadas. Como el directorio central del zip ubica cada parte directamente, leerlas cuesta unos pocos kilobytes sin importar cuán grande sea el libro. TZipFile e IXMLDocument, ambos en la RTL incluida, hacen todo el trabajo
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;
Dos detalles mantienen esto robusto en producción. Primero, las partes son opcionales: un paquete mínimo sin ningún docProps es perfectamente válido según ECMA-376, y por eso el código sondea con IndexOf en lugar de dar por sentado. Segundo, haga coincidir los elementos por nombre local y URI de espacio de nombres, como hace FindNode arriba, nunca por prefijo literal; dc: y cp: son convenciones del escritor de Excel, y los archivos producidos por otros generadores son libres de elegir prefijos distintos. Una nota de entorno: el proveedor predeterminado de IXMLDocument es MSXML, así que una aplicación de consola o un hilo de trabajo debe llamar a CoInitialize antes de LoadXMLData, o el primer análisis muere con un error COM
La hoja de costos, y cuándo una biblioteca supera a ambos analizadores
Medida en una máquina de desarrollo ordinaria, la ruta COM se ubica aproximadamente en dos a cuatro segundos por archivo cuando la sesión de automatización se crea por archivo, casi todo por el arranque de EXCEL.EXE más un análisis completo del libro, y requiere un Excel instalado y con licencia dondequiera que corra. Las dos rutas directas leen solo los contenedores de metadatos, terminan en milisegundos de un solo dígito por archivo y no necesitan nada instalado más allá de lo que un ejecutable Delphi ya enlaza. A lo largo de un recurso compartido de diez mil archivos, esa es la diferencia entre la mayor parte de una jornada laboral y menos de un minuto, sin ninguna pregunta de despliegue de Office de por medio
El inconveniente de las rutas directas es que son dos. Un pipeline que acepta ambos formatos mantiene dos analizadores con dos modos de falla disjuntos, páginas de códigos y tipos PROPVARIANT de un lado, espacios de nombres y partes opcionales del otro, y ninguno lee el formato del otro. Esa carga de mantenimiento es el argumento a favor de una biblioteca nativa: HotXLS, la biblioteca de hojas de cálculo en Object Pascal de losLab para Delphi y C++Builder en Windows, expone los mismos campos como propiedades simples del libro, Title, Author, Company, Created y el resto, rellenadas por Open tanto para .xls como para .xlsx, sin instalación de Excel y sin nada de la plomería de contenedores de arriba. Lee las propiedades como parte de una apertura completa del libro y no como un sondeo solo de metadatos, así que encaja en pipelines que de todos modos van a tocar los datos de las celdas; la superficie completa de propiedades en ambas fachadas, incluido el lado de escritura, se cubre en nuestro artículo sobre establecer propiedades de documentos Excel con HotXLS
Nota: las herramientas completas de análisis de Excel y extracción de metadatos están disponibles en el componente VCL HotXLS para Delphi