Demandez à un pipeline d'acheminer dix mille feuilles de calcul par auteur, entreprise ou date de dernière modification, et la pire chose qu'il puisse faire est d'ouvrir complètement chaque classeur. Les réponses se trouvent dans les propriétés de document du fichier, ce que le monde Office appelle Document Summary Information (Informations de résumé du document) : la couche de métadonnées que Windows Search indexe, sur laquelle SharePoint s'appuie pour classer, et qu'Excel affiche dans sa boîte de dialogue Propriétés. Cette couche fait quelques kilo-octets au maximum, et elle réside dans un endroit bien documenté dans les deux formats Excel. L'astuce consiste à l'atteindre depuis Delphi sans payer pour le million de cellules dont vous n'avez pas besoin
Il existe trois véritables voies, et elles diffèrent moins par ce qu'elles renvoient que par ce qu'elles exigent de la machine qui les exécute. L'automatisation COM pilote Excel lui-même et lit tout, aux prix de ressources d'un bureau. Le format .xls conserve ses propriétés dans des flux d'ensembles de propriétés OLE que Windows analysera pour vous. Le format .xlsx les conserve dans deux petites parties XML à l'intérieur d'un zip que la RTL Delphi peut ouvrir seule. Le code de travail pour chacun suit, avec les coûts clairement énoncés
Voie 1 : L'automatisation COM lit tout, aux prix des ressources d'un bureau
L'automatisation est la seule voie offrant une couverture totale via un seul modèle d'objet : l'ensemble de résumés standard, l'ensemble étendu avec l'entreprise (Company) et le responsable (Manager), et les propriétés personnalisées définies par l'utilisateur, toutes accessibles via BuiltinDocumentProperties et CustomDocumentProperties. Tout arrive sous la forme d'un OleVariant, et l'API a une habitude qu'il vaut mieux connaître avant qu'elle ne frappe : une propriété intégrée qui n'a jamais été assignée ne revient pas vide, elle déclenche une EOleException au moment où vous touchez Value. L'assistant ci-dessous traite cela comme "non défini" plutôt que comme un échec
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 := ''; // property exists but was never assigned
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // read-only
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; // reach this on every path, or EXCEL.EXE stays behind
Excel := Unassigned;
end;
end;
Maintenant l'addition. Excel doit être installé sur chaque machine où ce code s'exécute, ce qui exclut la plupart des serveurs en soi, et la politique de support de Microsoft indique explicitement qu'Office n'est ni conçu ni sous licence pour l'automatisation côté serveur sans surveillance. CreateOleObject lance un EXCEL.EXE complet et Workbooks.Open analyse l'intégralité du classeur, attendez-vous donc à environ deux à quatre secondes par fichier avant le retour de la première propriété. Et le try..finally autour de Quit n'est pas décoratif : une exception qui s'échappe entre CreateOleObject et Quit laisse un EXCEL.EXE orphelin qui maintient un verrou sur le fichier, invisible jusqu'à ce que la prochaine exécution échoue contre lui. Réutiliser une instance d'Excel pour un lot amortit le coût de démarrage mais concentre les risques, car une simple boîte de dialogue égarée sur le bureau caché bloque chaque fichier mis en file d'attente derrière elle
Voie 2 : .xls stocke les propriétés dans des flux d'ensembles de propriétés OLE
Un classeur BIFF8 est un fichier composé OLE, un système de fichiers miniature composé de stockages (storages) et de flux (streams). Les données des cellules se trouvent dans le flux Workbook ; les métadonnées se trouvent à côté, dans deux flux d'ensembles de propriétés dont les noms commencent par le caractère de contrôle #5 : \005SummaryInformation pour les champs classiques et \005DocumentSummaryInformation pour les champs étendus et personnalisés. À l'intérieur de chacun se trouve un ensemble de propriétés binaire au format MS-OLEPS, avec des sections indexées par un identifiant de format (FMTID) et des propriétés indexées par un ID de propriété entier. La section de résumé est FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, où PIDSI_TITLE est $02 et PIDSI_AUTHOR est $04 ; l'entreprise ($0F) et le responsable ($0E) résident dans la section document-summary, et les propriétés personnalisées dans une seconde section derrière un dictionnaire de noms
La bonne nouvelle est que sous Windows, vous n'analysez jamais ces octets vous-même. Le stockage structuré (Structured storage) expose les flux via IPropertySetStorage, et ce qui suit se compile tel quel avec les unités RTL standard
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: not present
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// usage: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Un mot honnête sur ce que cache cet extrait. Les chaînes peuvent arriver sous la forme VT_LPWSTR ou VT_LPSTR, et dans le cas ANSI, les octets sont encodés dans la propre page de codes de l'ensemble de propriétés, elle-même stockée en tant que propriété 1 de la section. Le cast (transtypage) ci-dessus n'est donc exact que lorsque cette page de codes correspond à celle du système. Les horodatages (timestamps) reviennent en tant que VT_FILETIME en UTC. Les propriétés personnalisées impliquent l'ouverture de la section définie par l'utilisateur, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, et le parcours de son dictionnaire de noms. IPropertyStorage absorbe tout cela sous Windows ; écrire votre propre analyseur MS-OLEPS pour un environnement sans stockage structuré est un véritable projet, pas l'affaire d'un après-midi
Voie 3 : .xlsx conserve les docProps au format XML dans le zip
C'est la voie dont la plupart des pipelines ont réellement besoin, puisque les nouveaux fichiers sont des .xlsx depuis près de deux décennies. Un classeur OOXML est un package zip, et ses propriétés sont réparties en petites parties selon leur objectif : docProps/core.xml contient les champs Dublin Core, dc:title, dc:creator, cp:lastModifiedBy, plus dcterms:created et dcterms:modified sous forme d'horodatages W3CDTF en UTC, tandis que docProps/app.xml contient des champs de niveau application tels que Company et AppVersion, et docProps/custom.xml contient les propriétés personnalisées. Parce que le répertoire central du zip localise directement chaque partie, leur lecture coûte quelques kilo-octets quelle que soit la taille du classeur. TZipFile et IXMLDocument, tous deux inclus dans la RTL fournie, font tout le travail
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;
Deux détails rendent cela robuste en production. Tout d'abord, les parties sont facultatives : un package minimal sans aucun docProps est parfaitement valide selon l'ECMA-376, c'est pourquoi le code teste avec IndexOf au lieu de supposer quoi que ce soit. Deuxièmement, faites correspondre les éléments par nom local et URI d'espace de noms, comme FindNode le fait ci-dessus, jamais par préfixe littéral ; dc: et cp: sont des conventions du rédacteur d'Excel, et les fichiers produits par d'autres générateurs sont libres de choisir des préfixes différents. Une note environnementale : le fournisseur IXMLDocument par défaut est MSXML, donc une application console ou un thread de travail doit appeler CoInitialize avant LoadXMLData, sinon la première analyse meurt avec une erreur COM
La feuille de coûts, et quand une bibliothèque bat les deux analyseurs
Mesurée sur une machine de développeur ordinaire, la voie COM prend environ deux à quatre secondes par fichier lorsque la session d'automatisation est créée par fichier, ce qui correspond presque entièrement au démarrage d'EXCEL.EXE et à une analyse complète du classeur, et cela nécessite un Excel installé et sous licence partout où il s'exécute. Les deux voies directes ne lisent que les conteneurs de métadonnées, se terminent en quelques millisecondes par fichier et ne nécessitent rien d'autre que ce qu'un exécutable Delphi intègre déjà (links in). Sur un partage de dix mille fichiers, c'est la différence entre la majeure partie d'une journée de travail et moins d'une minute, sans aucune question de déploiement d'Office associée
Le hic avec les voies directes, c'est qu'il y en a deux. Un pipeline qui accepte les deux formats maintient deux analyseurs avec deux modes de défaillance disjoints, les pages de codes et les types PROPVARIANT d'un côté, les espaces de noms et les parties facultatives de l'autre, et aucun ne lit le format de l'autre. Cette charge de maintenance est le propre d'une bibliothèque native : HotXLS, la bibliothèque de feuilles de calcul Object Pascal de losLab pour Delphi et C++Builder sous Windows, expose les mêmes champs que les propriétés de classeur ordinaires (Titre, Auteur, Entreprise, Créé, etc.), renseignés par Open pour .xls et .xlsx de la même manière, sans installation d'Excel et sans la plomberie des conteneurs ci-dessus. Elle lit les propriétés dans le cadre d'une ouverture complète du classeur plutôt que d'une analyse des métadonnées seules, ce qui convient aux pipelines qui vont de toute façon manipuler les données des cellules ; toute la surface des propriétés sur les deux façades, y compris le côté écriture, est couverte dans notre article sur la définition des propriétés de documents Excel avec HotXLS
Remarque : Des outils complets d'analyse Excel et d'extraction de métadonnées sont disponibles dans le composant VCL HotXLS