Naročite cevovodu, naj deset tisoč preglednic razvrsti po avtorju, podjetju ali datumu zadnje spremembe, in najslabše, kar lahko stori, je, da vsak delovni zvezek odpre v celoti. Odgovori tičijo v lastnostih dokumenta te datoteke, torej v tem, čemur svet Officea pravi Document Summary Information: v metapodatkovni plasti, ki jo indeksira Iskanje Windows, po kateri SharePoint razvršča in ki jo Excel prikaže v pogovornem oknu Lastnosti. Ta plast obsega kvečjemu nekaj kilobajtov in v obeh zapisih Excela živi na dobro dokumentiranem mestu. Trik je, kako do nje priti iz Delphija, ne da bi plačali za milijon celic, ki jih ne potrebujete
Prave poti so tri in razlikujejo se manj po tem, kaj vrnejo, kot po tem, kaj zahtevajo od stroja, ki jih poganja. Avtomatizacija COM poganja Excel sam in prebere vse, po namiznih cenah. Zapis .xls svoje lastnosti hrani v tokovih nabora lastnosti OLE, ki jih Windows razčleni namesto vas. Zapis .xlsx jih hrani v dveh majhnih delih XML znotraj datoteke zip, ki jo Delphijev RTL odpre sam. Sledi delujoča koda za vsako pot, s stroški, povedanimi naravnost
Pot 1: avtomatizacija COM prebere vse, po namiznih cenah
Avtomatizacija je edina pot s popolno pokritostjo prek enega objektnega modela: standardni povzetkni nabor, razširjeni nabor s poljema Company in Manager ter uporabniško določene lastnosti po meri, vse dosegljivo prek BuiltinDocumentProperties in CustomDocumentProperties. Vse pride kot OleVariant, API pa ima eno navado, ki jo je vredno poznati, preden ugrizne: vgrajena lastnost, ki ni bila nikoli dodeljena, se ne vrne prazna, temveč sproži EOleException v trenutku, ko se dotaknete Value. Spodnji pomočnik to obravnava kot "ni nastavljeno" in ne kot napako
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 := ''; // lastnost obstaja, a ni bila nikoli dodeljena
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // samo za branje
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; // dosezite to po vsaki poti, sicer EXCEL.EXE ostane zadaj
Excel := Unassigned;
end;
end;
Zdaj pa račun. Excel mora biti nameščen na vsakem stroju, kjer ta koda teče, kar samo po sebi izloči večino strežnikov, Microsoftova podporna politika pa izrecno pravi, da Office ni ne zasnovan ne licenciran za nenadzorovano avtomatizacijo na strani strežnika. CreateOleObject zažene celoten EXCEL.EXE, Workbooks.Open pa razčleni celoten delovni zvezek, zato pričakujte približno dve do štiri sekunde na datoteko, preden se vrne prva lastnost. In try..finally okoli Quit ni okras: izjema, ki uide med CreateOleObject in Quit, pusti osirotel EXCEL.EXE, ki drži zaklep nad datoteko, neviden, dokler naslednji tek ob njem ne odpove. Ponovna uporaba enega primerka Excela čez cel paket začetni strošek porazdeli, a tveganje zgosti, saj eno zablodelo pogovorno okno na skritem namizju ustavi vsako datoteko, ki čaka za njim
Pot 2: .xls lastnosti hrani v tokovih nabora lastnosti OLE
Delovni zvezek BIFF8 je sestavljena datoteka OLE, miniaturen datotečni sistem shramb in tokov. Podatki celic živijo v toku Workbook; metapodatki živijo ob njem v dveh tokovih nabora lastnosti, katerih imeni se začneta s krmilnim znakom #5: \005SummaryInformation za klasična polja in \005DocumentSummaryInformation za razširjena in tista po meri. V vsakem sedi binarni nabor lastnosti v razporeditvi MS-OLEPS, z razdelki, ključenimi po identifikatorju zapisa (FMTID), in lastnostmi, ključenimi po celoštevilskem ID lastnosti. Povzetkni razdelek je FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, kjer je PIDSI_TITLE enak $02, PIDSI_AUTHOR pa $04; Company ($0F) in Manager ($0E) živita v razdelku povzetka dokumenta, lastnosti po meri pa v drugem razdelku za slovarjem imen
Dobra novica je, da v sistemu Windows teh bajtov nikoli ne razčlenjujete sami. Strukturirana shramba tokove razkrije prek IPropertySetStorage, spodnja koda pa se prevede tako, kot je prikazana, z običajnimi enotami 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: ni prisotno
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// raba: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Pošteno o tem, kaj izsek skriva. Nizi lahko pridejo kot VT_LPWSTR ali kot VT_LPSTR, v primeru ANSI pa so bajti kodirani v lastni kodni strani nabora lastnosti, ki je sama shranjena kot lastnost 1 razdelka, zato je zgornja pretvorba točna le, kadar se ta kodna stran ujema s sistemsko. Časovni žigi se vrnejo kot VT_FILETIME v UTC. Lastnosti po meri pomenijo odpiranje uporabniško določenega razdelka, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, in prehod njegovega slovarja imen. IPropertyStorage v sistemu Windows vse to prevzame nase; pisanje lastnega razčlenjevalnika MS-OLEPS za okolje brez strukturirane shrambe je pravi projekt in ne popoldanska naloga
Pot 3: .xlsx hrani docProps kot XML znotraj zip
To je pot, ki jo večina cevovodov v resnici potrebuje, saj so nove datoteke že skoraj dve desetletji .xlsx. Delovni zvezek OOXML je paket zip, njegove lastnosti pa so po namenu razdeljene med majhne dele: docProps/core.xml drži polja Dublin Core, dc:title, dc:creator, cp:lastModifiedBy ter dcterms:created in dcterms:modified kot časovna žiga W3CDTF v UTC, docProps/app.xml drži polja na ravni aplikacije, kot sta Company in AppVersion, docProps/custom.xml pa lastnosti po meri. Ker centralni imenik datoteke zip vsak del najde neposredno, njihovo branje stane nekaj kilobajtov ne glede na to, kako velik je delovni zvezek. TZipFile in IXMLDocument, oba priložena v RTL, opravita vse delo
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;
Dve podrobnosti to v produkciji ohranjata robustno. Prvič, deli so neobvezni: minimalen paket povsem brez docProps je po ECMA-376 popolnoma veljaven, in prav zato koda z IndexOf preveri, namesto da bi predpostavljala. Drugič, elemente ujemajte po lokalnem imenu in URI imenskega prostora, kot to zgoraj počne FindNode, nikoli po dobesedni predponi; dc: in cp: sta dogovora Excelovega pisalnika, datoteke iz drugih generatorjev pa smejo izbrati druge predpone. Ena okoljska opomba: privzeti ponudnik za IXMLDocument je MSXML, zato mora konzolna aplikacija ali delovna nit pred LoadXMLData poklicati CoInitialize, sicer prvo razčlenjevanje umre z napako COM
Stroškovnik in kdaj knjižnica premaga oba razčlenjevalnika
Izmerjeno na običajnem razvijalskem stroju pot COM pristane pri približno dveh do štirih sekundah na datoteko, kadar se seja avtomatizacije ustvari za vsako datoteko posebej, in skoraj vse to je zagon EXCEL.EXE plus polna razčlenitev delovnega zvezka, povrhu pa povsod, kjer teče, zahteva nameščen in licenciran Excel. Neposredni poti preberejo le vsebnike metapodatkov, končata v enomestnem številu milisekund na datoteko in ne potrebujeta ničesar nameščenega poleg tega, kar izvedljiva datoteka v Delphiju že vključuje. Na deljenem mestu z desetimi tisoč datotekami je to razlika med večjim delom delovnega dne in manj kot minuto, brez vsakršnega vprašanja o nameščanju Officea
Zadrega pri neposrednih poteh je, da sta dve. Cevovod, ki sprejema oba zapisa, vzdržuje dva razčlenjevalnika z dvema nepovezanima načinoma odpovedi, na eni strani kodne strani in tipi PROPVARIANT, na drugi imenski prostori in neobvezni deli, in nobeden ne bere zapisa drugega. Prav to vzdrževalno breme je argument za izvorno knjižnico: HotXLS, knjižnica losLab za preglednice v Object Pascalu za Delphi in C++Builder v sistemu Windows, ista polja razkriva kot navadne lastnosti delovnega zvezka, Title, Author, Company, Created in preostale, ki jih Open napolni enako za .xls in .xlsx, brez nameščenega Excela in brez vse zgornje vodovodne napeljave okoli vsebnikov. Lastnosti bere kot del polnega odpiranja delovnega zvezka in ne kot sondo samo za metapodatke, zato se prilega cevovodom, ki se tako ali tako lotijo tudi podatkov celic; celotna površina lastnosti na obeh fasadah, vključno s pisalno stranjo, je obravnavana v našem članku o nastavljanju lastnosti dokumenta Excel s HotXLS
Opomba: celovita orodja za razčlenjevanje Excela in izvleček metapodatkov so na voljo v komponenti HotXLS Delphi VCL