Műszaki cikk

Excel dokumentumtulajdonságok Delphiben: három út

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

Az Excel Document Summary Information eléréséhez vezető három Delphi út ábrája: COM automatizálás, amely magát az Excelt vezérli, OLE tulajdonságkészlet-folyamok xls fájlokhoz, és OOXML docProps XML-elemzés xlsx csomagokhoz
A COM automatizálás teljes lefedettséget vesz licencelt asztali Excel és fájlonként másodpercek árán, míg a két formátumhoz igazodó út ezredmásodpercek alatt csak a jellemzőtárolókat olvassa. Amit az egyes utak visszaadnak, az szinte azonos — amit a gazdagéptől követelnek, az nem

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

Egy BIFF8 xls összetett fájl felépítése Delphi szemszögből, amely a Workbook folyamot a SummaryInformation és a DocumentSummaryInformation tulajdonságkészletek mellé helyezi, a StgOpenStorageEx hívástól az IPropertySetStorage felületig tartó elérési lánccal
Az xls fájl a cellaadatokat és a dokumentumtulajdonságokat testvérfolyamokként tárolja egy OLE összetett fájlban. A Windows elemzi Ön helyett a bináris tulajdonságkészleteket, így a Delphi kód sem az MS-OLEPS elrendezéshez, sem a kódlapokhoz nem nyúl kézzel

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

Delphi: egy xlsx zipcsomag elrendezése, amely a docProps core, app és custom XML tagjait mutatja a munkalaprészek mellett, az opcionális részek vizsgálatára és a névterek egyeztetésére vonatkozó éles szabályokkal
A munkalapadatok uralják az xlsx csomagot, a jellemzők mégis három kis, nem kötelező tagban ülnek mellettük. A zip központi könyvtárán át való közvetlen elérés a tulajdonságokkal arányossá teszi az olvasást, nem a munkafüzettel
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