Atunci când procesați loturi mari de foi de calcul Excel într-un flux automatizat, rareori doriți să încărcați întregul document în memorie doar pentru a afla ce este. Adesea, metadatele încorporate în fișier (autorul, titlul, data creării și proprietățile personalizate) sunt suficiente pentru a direcționa, indexa sau respinge documentul. În lumea Microsoft Office, aceste metadate sunt cunoscute sub numele de Document Summary Information (Informații de sumar ale documentului)
Extragerea acestor informații nativ în Delphi fără a vă baza pe automatizarea OLE (care necesită ca Excel să fie instalat pe mașina gazdă) necesită parsarea directă a structurii fișierului de bază. În acest articol, vom analiza modul în care funcționează sumarele de documente în fișierele Excel și cum să le extragem eficient utilizând parsarea directă a fluxurilor
Înțelegerea fluxurilor de metadate Excel
Din punct de vedere istoric, fișierele Excel mai vechi (.xls) sunt stocate în formate OLE Compound Document, acționând efectiv ca niște mini-sisteme de fișiere care conțin fluxuri și zone de stocare. Metadatele sunt găzduite în două fluxuri specifice:
SummaryInformation: Conține proprietăți standard precum Title, Subject, Author, Keywords și Revision NumberDocumentSummaryInformation: Conține proprietăți extinse, cum ar fi Company, Manager și proprietăți personalizate definite de utilizator
Fișierele Excel moderne (.xlsx) utilizează formatul Office Open XML (OOXML), care este o structură XML arhivată tip ZIP. Metadatele aici se află în docProps/core.xml, docProps/app.xml și docProps/custom.xml. O componentă robustă de parsare Delphi trebuie să gestioneze impecabil ambele structuri interne, expunând în același timp un API unificat dezvoltatorului
Parsarea documentelor compuse OLE în Delphi
Pentru a citi informațiile de sumar dintr-un fișier `.xls` moștenit fără instrumente de la terți, trebuie să parsați stocarea structurată OLE (OLE Structured Storage). Microsoft expune acest lucru prin interfața COM IPropertySetStorage. Iată o implementare Delphi de bază care evită pornirea Excel:
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;
Extragerea programatică cu HotXLS
Deși API-ul Windows COM funcționează pentru fișierele `.xls`, nu funcționează pentru fișierele moderne `.xlsx` (care sunt arhive ZIP). Mai mult, utilizarea API-ului COM pe mai multe platforme (de exemplu, pe Linux sau macOS prin FireMonkey) este imposibilă. Actualizările recente ale componentei HotXLS au introdus unități dedicate (de exemplu, lxXlsSummary) pentru a izola și a optimiza citirea acestor fluxuri de sumar în ambele formate, complet nativ în cod Delphi
Un exemplu multiplatformă
Utilizând interfețele XlsReadDocumentSummaryInformation și XlsReadSummaryInformation, puteți prelua rapid metadatele tip șir din ambele formate, `.xls` și `.xlsx`, fără să vă faceți griji cu privire la arhitectura sistemului de fișiere subiacent
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));
De ce contează extragerea dedicată a sumarului
Beneficiul principal al acestei abordări este performanța și siguranța memoriei. Evitând instanțierea întregului DOM (Document Object Model) al registrului de lucru și parsând doar docProps/core.xml sau fluxurile de proprietăți OLE, amprenta aplicației dumneavoastră rămâne incredibil de mică. Dacă indexați 10.000 de fișiere Excel într-o rețea partajată, încercarea de a parsa complet fiecare fișier vă va epuiza memoria și va dura ore întregi. Extragerea dedicată a sumarului finalizează aceeași sarcină în câteva secunde
Mai mult, citirea nativă a fluxurilor asigură faptul că aplicația dumneavoastră poate rula ca un serviciu de fundal sau pe un server Linux fără interfață grafică (headless), fără a apela vreodată Excel.exe, o cerință critică pentru arhitecturile scalabile moderne
Notă: Instrumente cuprinzătoare pentru parsarea Excel și extragerea metadatelor sunt disponibile în HotXLS VCL Component
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;