Kérjen meg egy munkafolyamatot, hogy tízezer táblázatot szerző, cég vagy utolsó módosítási dátum szerint irányítson, és a legrosszabb, amit tehet, hogy mindegyik munkafüzetet teljesen megnyitja. A válaszok a fájl dokumentumtulajdonságaiban utaznak, amelyet az Office világa Document Summary Information néven ismer: ez az a jellemzőréteg, amelyet a Windows Search indexel, amely szerint a SharePoint rendszerez, és amelyet az Excel a Tulajdonságok párbeszédpanelen mutat. Ez a réteg legfeljebb néhány kilobájt, és mindkét Excel-formátumban jól dokumentált helyen él. A trükk az, hogyan éri el Delphiből anélkül, hogy megfizetné azt a millió cellát, amelyre nincs szüksége
Három valódi út van, és nem annyira abban különböznek, mit adnak vissza, mint inkább abban, mit követelnek a gépről, amely futtatja őket. A COM automatizálás magát az Excelt vezérli, és mindent elolvas, asztali áron. Az .xls formátum a tulajdonságait OLE tulajdonságkészlet-folyamokban tartja, amelyeket a Windows elemez Ön helyett. Az .xlsx formátum két kis XML-részben tartja őket egy zipen belül, amelyet a Delphi RTL magától meg tud nyitni. Mindegyikhez működő kód következik, a költségek nyílt kimondásával
1. út: a COM automatizálás mindent elolvas, asztali áron
Az automatizálás az egyetlen út, amely egyetlen objektummodellen át teljes lefedettséget ad: a szabványos összegzőkészletet, a Company és Manager mezőt is tartalmazó kiterjesztett készletet, valamint a felhasználó által megadott egyedi tulajdonságokat, mind elérhetően a BuiltinDocumentProperties és a CustomDocumentProperties gyűjteményen át. Minden OleVariant típusban érkezik, és az API-nak van egy szokása, amelyet érdemes megismerni, mielőtt harapna: az a beépített tulajdonság, amelyhez soha nem rendeltek értéket, nem üresen jön vissza, hanem EOleException kivételt vált ki abban a pillanatban, amikor a Value értékéhez nyúl. Az alábbi segédfüggvény ezt „nincs beállítva” esetként, nem hibaként kezeli
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 := ''; // a tulajdonság létezik, de soha nem kapott értéket
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // csak olvasható
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; // minden úton ide kell jutni, különben az EXCEL.EXE ottmarad
Excel := Unassigned;
end;
end;
Most jön a számla. Az Excelt telepíteni kell minden gépre, amelyen ez a kód fut, ami önmagában kizárja a legtöbb kiszolgálót, a Microsoft támogatási irányelve pedig kifejezetten kimondja, hogy az Office nem felügyelet nélküli, kiszolgálóoldali automatizálásra készült, és nem is arra licencelt. A CreateOleObject teljes EXCEL.EXE példányt indít, a Workbooks.Open pedig a teljes munkafüzetet elemzi, tehát fájlonként nagyjából két-négy másodpercre számítson, mielőtt az első tulajdonság visszaérkezne. A Quit köré tett try..finally pedig nem díszítés: az a kivétel, amely a CreateOleObject és a Quit között szökik meg, árván hagyott EXCEL.EXE példányt hagy maga után, amely zárat tart a fájlon, és láthatatlan marad addig, amíg a következő futás el nem hasal rajta. Ha egyetlen Excel-példányt használ újra egy kötegben, azzal elosztja az indítási költséget, viszont sűríti a kockázatot, mert egyetlen elkóborolt párbeszédpanel a rejtett asztalon minden mögötte sorban álló fájlt megakaszt
2. út: az .xls a tulajdonságokat OLE tulajdonságkészlet-folyamokban tárolja
A BIFF8 munkafüzet OLE összetett fájl, tárolók és folyamok miniatűr fájlrendszere. A cellaadatok a Workbook folyamban élnek; a jellemzők mellette, két olyan tulajdonságkészlet-folyamban, amelyek neve az #5 vezérlőkarakterrel kezdődik: a \005SummaryInformation a klasszikus mezőké, a \005DocumentSummaryInformation pedig a kiterjesztett és egyedi mezőké. Mindegyikben bináris tulajdonságkészlet ül MS-OLEPS elrendezésben, ahol a szakaszok kulcsa egy formátumazonosító (FMTID), a tulajdonságoké pedig egy egész számú tulajdonságazonosító. Az összegzőszakasz FMTID értéke {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, ahol a PIDSI_TITLE értéke $02, a PIDSI_AUTHOR értéke pedig $04; a Company ($0F) és a Manager ($0E) a dokumentumösszegző szakaszban él, az egyedi tulajdonságok pedig egy második szakaszban, névszótár mögött
A jó hír az, hogy Windows alatt soha nem kell magának elemeznie ezeket a bájtokat. A strukturált tároló az IPropertySetStorage felületen át teszi elérhetővé a folyamokat, és az alábbi kód a gyári RTL egységekkel a bemutatott formában fordul
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: nincs jelen
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// használat: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Egy őszinte szó arról, amit a részlet elrejt. A karakterláncok VT_LPWSTR vagy VT_LPSTR típusban érkezhetnek, és az ANSI esetben a bájtok a tulajdonságkészlet saját kódlapján vannak kódolva, amely maga a szakasz 1-es tulajdonságaként van tárolva, tehát a fenti típuskonverzió csak akkor pontos, ha ez a kódlap egyezik a rendszerével. Az időbélyegek VT_FILETIME típusban, UTC szerint jönnek vissza. Az egyedi tulajdonságok a felhasználó által megadott szakasz megnyitását jelentik — FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE} —, majd a névszótárának bejárását. Az IPropertyStorage Windows alatt mindezt magába szívja; saját MS-OLEPS elemző írása olyan környezethez, amelyben nincs strukturált tároló, valódi projekt, nem délutáni munka
3. út: az .xlsx a docProps részeket XML-ként tartja a zipen belül
Ez az az út, amelyre a legtöbb munkafolyamatnak valójában szüksége van, hiszen az új fájlok közel két évtizede .xlsx formátumúak. Az OOXML munkafüzet zipcsomag, a tulajdonságai pedig cél szerint kis részekre oszlanak: a docProps/core.xml tartja a Dublin Core mezőket, a dc:title, dc:creator és cp:lastModifiedBy elemeket, valamint a dcterms:created és dcterms:modified mezőket W3CDTF időbélyegként, UTC szerint, míg a docProps/app.xml az alkalmazásszintű mezőket, például a Company és az AppVersion értéket tartja, a docProps/custom.xml pedig az egyedi tulajdonságokat. Mivel a zip központi könyvtára közvetlenül helyezi el az egyes részeket, az olvasásuk néhány kilobájtba kerül, bármekkora is a munkafüzet. A TZipFile és az IXMLDocument, mindkettő a szállított RTL része, elvégzi a teljes munkát
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;
Két részlet tartja ezt éles üzemben szilárdan. Először: a részek nem kötelezők — egy minimális csomag, amelyben egyáltalán nincs docProps, tökéletesen érvényes az ECMA-376 szerint, és éppen ezért vizsgálódik a kód IndexOf hívással feltételezés helyett. Másodszor: az elemeket helyi név és névtér-URI alapján egyeztesse, ahogy a fenti FindNode teszi, soha ne szó szerinti előtag alapján; a dc: és a cp: az Excel írójának szokása, és a más generátorok által előállított fájlok szabadon választhatnak más előtagot. Egy környezeti megjegyzés: az IXMLDocument alapértelmezett szállítója az MSXML, tehát egy konzolalkalmazásnak vagy munkaszálnak a LoadXMLData előtt meg kell hívnia a CoInitialize függvényt, különben az első elemzés COM-hibával hal meg
A költséglap, és mikor ver mindkét elemzőt egy könyvtár
Egy átlagos fejlesztői gépen mérve a COM-út fájlonként nagyjából két-négy másodpercnél áll meg, ha az automatizálási munkamenet fájlonként jön létre, és ennek szinte az egésze az EXCEL.EXE indulása plusz a teljes munkafüzet elemzése, ráadásul telepített, licencelt Excelt kíván mindenütt, ahol fut. A két közvetlen út csak a jellemzőtárolókat olvassa, fájlonként egy számjegyű ezredmásodperc alatt végez, és semmi olyat nem igényel telepítve, amit egy Delphi végrehajtható állomány ne szerkesztene magába. Tízezer fájlos megosztáson ez egy majdnem teljes munkanap és egy percen belüli futás közti különbség, minden Office-telepítési kérdés nélkül
A közvetlen utak bökkenője az, hogy kettő van belőlük. Az a munkafolyamat, amely mindkét formátumot fogadja, két elemzőt tart karban két, egymástól független hibamóddal — az egyik oldalon kódlapokkal és PROPVARIANT típusokkal, a másikon névterekkel és nem kötelező részekkel —, és egyik sem olvassa a másik formátumát. Ez a karbantartási teher az érv egy natív könyvtár mellett: a HotXLS, a losLab Object Pascal táblázatkönyvtára Delphihez és C++Builderhez Windows alatt, ugyanezeket a mezőket egyszerű munkafüzet-tulajdonságként teszi elérhetővé — Title, Author, Company, Created és a többi —, amelyeket az Open tölt fel .xls és .xlsx esetén egyaránt, Excel-telepítés és a fenti tárolóvezetékek nélkül. A tulajdonságokat a teljes munkafüzet megnyitásának részeként olvassa, nem csak jellemzőkre szorítkozó vizsgálatként, tehát azokhoz a munkafolyamatokhoz illik, amelyek amúgy is hozzányúlnak a cellaadatokhoz; a mindkét felületen elérhető teljes tulajdonságfelületet, az írási oldalt is beleértve, az Excel dokumentumtulajdonságainak HotXLS-szel való beállításáról szóló cikkünk tárgyalja
Megjegyzés: a teljes Excel-elemzési és jellemzőkinyerési eszközök a HotXLS Delphi VCL komponensben érhetők el