Cereți unui flux de procesare să direcționeze zece mii de foi de calcul după autor, companie sau data ultimei modificări, iar cel mai rău lucru pe care îl poate face este să deschidă complet fiecare registru de lucru. Răspunsurile călătoresc în proprietățile de document ale fișierului, ceea ce lumea Office numește Document Summary Information: stratul de metadate pe care Windows Search îl indexează, după care SharePoint clasifică fișierele și pe care Excel îl arată în dialogul său Properties. Acel strat are cel mult câțiva kiloocteți și stă într-un loc bine documentat în ambele formate Excel. Șmecheria este să ajungeți la el din Delphi fără să plătiți pentru milionul de celule de care nu aveți nevoie
Există trei rute reale, iar ele diferă mai puțin prin ceea ce returnează decât prin ceea ce pretind de la mașina care le rulează. Automatizarea COM pilotează Excel însuși și citește totul, la prețuri de desktop. Formatul .xls își păstrează proprietățile în fluxuri OLE de tip property-set pe care Windows le analizează în locul dumneavoastră. Formatul .xlsx le păstrează în două părți XML mici din interiorul unei arhive zip pe care RTL-ul Delphi o poate deschide singur. Urmează cod funcțional pentru fiecare, cu costurile spuse pe față
Ruta 1: automatizarea COM citește tot, la prețuri de desktop
Automatizarea este singura rută cu acoperire totală printr-un singur model de obiecte: setul standard de sumar, setul extins cu Company și Manager și proprietățile personalizate definite de utilizator, toate accesibile prin BuiltinDocumentProperties și CustomDocumentProperties. Totul sosește ca OleVariant, iar API-ul are un obicei pe care merită să îl știți înainte să vă muște: o proprietate încorporată care nu a fost niciodată atribuită nu se întoarce goală, ci ridică o EOleException în clipa în care atingeți Value. Funcția ajutătoare de mai jos tratează asta drept „nesetat”, nu drept eșec
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 := ''; // proprietatea există, dar nu a fost niciodată atribuită
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // doar citire
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; // ajungeți aici pe orice cale, altfel EXCEL.EXE rămâne în urmă
Excel := Unassigned;
end;
end;
Acum și nota de plată. Excel trebuie instalat pe fiecare mașină pe care rulează acest cod, ceea ce exclude prin sine majoritatea serverelor, iar politica de suport Microsoft spune explicit că Office nu este nici proiectat, nici licențiat pentru automatizare nesupravegheată pe partea de server. CreateOleObject lansează un EXCEL.EXE complet, iar Workbooks.Open analizează întregul registru de lucru, deci așteptați-vă la aproximativ două până la patru secunde pe fișier până când se întoarce prima proprietate. Iar try..finally din jurul lui Quit nu este decorativ: o excepție care scapă între CreateOleObject și Quit lasă în urmă un EXCEL.EXE orfan care ține fișierul blocat, invizibil până când următoarea rulare eșuează din cauza lui. Reutilizarea unei singure instanțe Excel pe tot lotul amortizează costul de pornire, dar concentrează riscul, pentru că un singur dialog rătăcit pe desktopul ascuns blochează fiecare fișier din coada de după el
Ruta 2: .xls stochează proprietățile în fluxuri OLE property-set
Un registru de lucru BIFF8 este un fișier compus OLE, un sistem de fișiere în miniatură, cu depozite și fluxuri. Datele celulelor stau în fluxul Workbook; metadatele stau alături, în două fluxuri de tip property-set ale căror nume încep cu caracterul de control #5: \005SummaryInformation pentru câmpurile clasice și \005DocumentSummaryInformation pentru cele extinse și personalizate. În fiecare stă un set binar de proprietăți în aranjamentul MS-OLEPS, cu secțiuni cheiate după un identificator de format (FMTID) și proprietăți cheiate după un ID întreg de proprietate. Secțiunea de sumar are FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, unde PIDSI_TITLE este $02, iar PIDSI_AUTHOR este $04; Company ($0F) și Manager ($0E) stau în secțiunea de sumar al documentului, iar proprietățile personalizate într-o a doua secțiune, în spatele unui dicționar de nume
Vestea bună este că pe Windows nu analizați niciodată acei octeți chiar dumneavoastră. Stocarea structurată expune fluxurile prin IPropertySetStorage, iar codul următor compilează așa cum este scris, cu unitățile RTL din livrare
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: nu există
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// utilizare: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
O vorbă cinstită despre ce ascunde fragmentul. Șirurile pot sosi ca VT_LPWSTR sau ca VT_LPSTR, iar în cazul ANSI octeții sunt codificați în pagina de coduri proprie a setului de proprietăți, stocată ea însăși ca proprietatea 1 a secțiunii, așa că turnarea de mai sus este exactă doar când acea pagină de coduri coincide cu cea a sistemului. Marcajele de timp se întorc ca VT_FILETIME în UTC. Proprietățile personalizate înseamnă deschiderea secțiunii definite de utilizator, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, și parcurgerea dicționarului ei de nume. IPropertyStorage absoarbe tot acest efort pe Windows; scrierea propriului analizor MS-OLEPS pentru un mediu fără stocare structurată este un proiect adevărat, nu o după-amiază de lucru
Ruta 3: .xlsx păstrează docProps ca XML în interiorul arhivei zip
Aceasta este ruta de care au nevoie de fapt majoritatea fluxurilor, de vreme ce fișierele noi sunt .xlsx de aproape două decenii. Un registru de lucru OOXML este un pachet zip, iar proprietățile lui sunt împărțite pe părți mici, după scop: docProps/core.xml conține câmpurile Dublin Core, dc:title, dc:creator, cp:lastModifiedBy, plus dcterms:created și dcterms:modified ca marcaje de timp W3CDTF în UTC, în timp ce docProps/app.xml conține câmpuri la nivel de aplicație, precum Company și AppVersion, iar docProps/custom.xml conține proprietățile personalizate. Fiindcă directorul central al arhivei zip localizează direct fiecare parte, citirea lor costă câțiva kiloocteți, oricât de mare ar fi registrul de lucru. TZipFile și IXMLDocument, ambele din RTL-ul livrat, fac toată treaba
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;
Două detalii țin acest cod robust în producție. Întâi, părțile sunt opționale: un pachet minimal, fără niciun docProps, este perfect valid conform ECMA-376, motiv pentru care codul sondează cu IndexOf în loc să presupună. Apoi, potriviți elementele după numele local și URI-ul spațiului de nume, așa cum face FindNode mai sus, niciodată după prefixul literal; dc: și cp: sunt convenții ale scriitorului Excel, iar fișierele produse de alte generatoare au voie să aleagă alte prefixe. O notă de mediu: furnizorul implicit pentru IXMLDocument este MSXML, deci o aplicație de consolă sau un fir de execuție lucrător trebuie să apeleze CoInitialize înainte de LoadXMLData, altfel prima analiză moare cu o eroare COM
Fișa de costuri și momentul în care o bibliotecă bate ambele analizoare
Măsurată pe o mașină obișnuită de dezvoltare, ruta COM ajunge la aproximativ două până la patru secunde pe fișier atunci când sesiunea de automatizare este creată pentru fiecare fișier, aproape totul fiind pornirea EXCEL.EXE plus analiza completă a registrului de lucru, și cere un Excel instalat și licențiat oriunde rulează. Cele două rute directe citesc doar containerele de metadate, se termină în milisecunde exprimate cu o singură cifră pe fișier și nu au nevoie de nimic instalat dincolo de ce leagă deja un executabil Delphi. Pe o partajare cu zece mii de fișiere, aceasta este diferența dintre mai toată o zi de lucru și sub un minut, fără nicio întrebare de implementare Office atașată
Problema cu rutele directe este că sunt două. Un flux care acceptă ambele formate întreține două analizoare cu două moduri de eșec disjuncte, pagini de coduri și tipuri PROPVARIANT pe o parte, spații de nume și părți opționale pe cealaltă, iar niciunul nu citește formatul celuilalt. Acea povară de întreținere este argumentul pentru o bibliotecă nativă: HotXLS, biblioteca Object Pascal de foi de calcul de la losLab pentru Delphi și C++Builder pe Windows, expune aceleași câmpuri ca simple proprietăți de registru de lucru, Title, Author, Company, Created și restul, populate de Open deopotrivă pentru .xls și .xlsx, fără instalare de Excel și fără nimic din instalația de containere de mai sus. Ea citește proprietățile ca parte a deschiderii complete a registrului de lucru, nu ca pe o sondare doar de metadate, așa că se potrivește fluxurilor care oricum ajung să atingă datele celulelor; suprafața completă de proprietăți pe ambele fațade, inclusiv partea de scriere, este tratată în articolul nostru despre setarea proprietăților de document Excel cu HotXLS
Notă: instrumentele complete de analiză Excel și de extragere a metadatelor sunt disponibile în HotXLS Delphi VCL Component