Techninis straipsnis

Excel dokumento savybių skaitymas Delphi: trys keliai

Paprašykite grandinės surūšiuoti dešimt tūkstančių skaičiuoklių pagal autorių, įmonę ar paskutinio keitimo datą, ir blogiausia, ką ji gali padaryti, yra pilnai atverti kiekvieną darbo knygą. Atsakymai keliauja failo dokumento savybėse, tai, ką Office pasaulis vadina Document Summary Information: metaduomenų sluoksniu, kurį indeksuoja Windows Search, pagal kurį rikiuoja SharePoint ir kurį Excel rodo savo savybių dialoge. Tas sluoksnis užima daugiausia kelis kilobaitus ir abiejuose Excel formatuose gyvena gerai dokumentuotoje vietoje. Visas triukas yra jį pasiekti iš Delphi nemokant už milijoną langelių, kurių jums nereikia

Yra trys tikri keliai, ir jie skiriasi mažiau tuo, ką grąžina, nei tuo, ko reikalauja iš mašinos, kurioje veikia. COM automatizavimas valdo patį Excel ir perskaito viską, tik už stalinio kompiuterio kainą. .xls formatas savo savybes laiko OLE savybių rinkinių srautuose, kuriuos Windows išanalizuos už jus. .xlsx formatas jas laiko dviejose mažose XML dalyse zip archyve, kurį Delphi RTL atveria savarankiškai. Toliau pateikiamas veikiantis kodas kiekvienam keliui su atvirai įvardytomis kainomis

Trijų Delphi kelių prie Excel Document Summary Information schema: COM automatizavimas, valdantis patį Excel, OLE savybių rinkinių srautai xls failams ir OOXML docProps XML analizė xlsx paketams
COM automatizavimas nuperka visišką aprėptį licencijuoto stalinio Excel ir sekundžių vienam failui kaina, o du formatui gimtieji keliai skaito tik metaduomenų talpyklas per milisekundes. Tai, ką kiekvienas kelias grąžina, beveik sutampa — tai, ko jis reikalauja iš mašinos, ne

1 kelias: COM automatizavimas perskaito viską už stalinio kompiuterio kainą

Automatizavimas yra vienintelis kelias su visiška aprėptimi per vieną objektų modelį: standartinis santraukos rinkinys, išplėstinis rinkinys su Company ir Manager bei naudotojo apibrėžtos savos savybės, visos pasiekiamos per BuiltinDocumentProperties ir CustomDocumentProperties. Viskas atkeliauja kaip OleVariant, o šis API turi vieną įprotį, kurį verta žinoti prieš jam įkandant: niekada nepriskirta įtaisytoji savybė negrįžta tuščia, ji sukelia EOleException tą pačią akimirką, kai paliečiate Value. Žemiau pateikta pagalbinė funkcija tai traktuoja kaip „nenustatyta“, o ne kaip nesėkmę

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 := '';   // savybė egzistuoja, bet niekada nebuvo priskirta
    end;
  end;

begin
  Excel := CreateOleObject('Excel.Application');
  try
    Excel.DisplayAlerts := False;
    Book := Excel.Workbooks.Open(FileName, 0, True);   // tik skaitymui
    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;   // pasiekite tai bet kuriuo keliu, kitaip EXCEL.EXE liks kaboti
    Excel := Unassigned;
  end;
end;

Dabar sąskaita. Excel privalo būti įdiegtas kiekvienoje mašinoje, kurioje šis kodas veikia, o tai vien savaime atmeta daugumą serverių, ir Microsoft palaikymo politika aiškiai sako, kad Office nėra nei suprojektuotas, nei licencijuotas neprižiūrimam serverio pusės automatizavimui. CreateOleObject paleidžia pilną EXCEL.EXE, o Workbooks.Open išanalizuoja visą darbo knygą, todėl tikėkitės maždaug dviejų–keturių sekundžių vienam failui, kol grįš pirmoji savybė. O try..finally aplink Quit nėra papuošalas: išimtis, pasprukusi tarp CreateOleObject ir Quit, palieka našlaitį EXCEL.EXE, laikantį failo užraktą, nematomą tol, kol kitas paleidimas prie jo neužklius. Vieno Excel egzemplioriaus naudojimas visai paketai paskirsto paleidimo kainą, bet sutelkia riziką, nes vienas paklydęs dialogas paslėptame darbastalyje sustabdo kiekvieną už jo eilėje laukiantį failą

2 kelias: .xls savybes laiko OLE savybių rinkinių srautuose

BIFF8 darbo knyga yra OLE sudėtinis failas, miniatiūrinė saugyklų ir srautų failų sistema. Langelių duomenys gyvena Workbook sraute; metaduomenys gyvena šalia jo dviejuose savybių rinkinių srautuose, kurių vardai prasideda valdymo simboliu numeris 5: \005SummaryInformation klasikiniams laukams ir \005DocumentSummaryInformation išplėstiniams bei saviems. Kiekviename jų sėdi dvejetainis savybių rinkinys MS-OLEPS išdėstyme su sekcijomis, raktuojamomis formato identifikatoriumi (FMTID), ir savybėmis, raktuojamomis sveikuoju savybės ID. Santraukos sekcija yra FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, kurioje PIDSI_TITLE yra $02, o PIDSI_AUTHOR yra $04; Company ($0F) ir Manager ($0E) gyvena dokumento santraukos sekcijoje, o savos savybės – antroje sekcijoje už vardų žodyno

Delphi: BIFF8 xls sudėtinio failo anatomija, kurioje Workbook srautas yra šalia SummaryInformation ir DocumentSummaryInformation savybių rinkinių, su StgOpenStorageEx į IPropertySetStorage prieigos grandine
xls failas langelių duomenis ir dokumento savybes saugo kaip gretimus srautus OLE sudėtiniame faile. Windows dvejetainius savybių rinkinius išanalizuos už jus, todėl Delphi kodas ranka neliečia nei MS-OLEPS išdėstymų, nei kodų puslapių

Gera žinia ta, kad Windows aplinkoje tų baitų patiems analizuoti niekada nereikia. Struktūrinė saugykla srautus atskleidžia per IPropertySetStorage, o toliau pateiktas kodas su standartiniais RTL moduliais kompiliuojasi taip, kaip parašytas

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: nėra
  try
    case Value.vt of
      VT_LPSTR:  Result := string(AnsiString(Value.pszVal));
      VT_LPWSTR: Result := Value.pwszVal;
    end;
  finally
    PropVariantClear(Value);
  end;
end;

// naudojimas: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));

Sąžiningas žodis apie tai, ką ši ištrauka nutyli. Eilutės gali atkeliauti kaip VT_LPWSTR arba kaip VT_LPSTR, o ANSI atveju baitai yra užkoduoti paties savybių rinkinio kodų puslapiu, kuris pats saugomas kaip sekcijos savybė numeris 1, todėl aukščiau esantis tipo keitimas tikslus tik tada, kai tas kodų puslapis sutampa su sistemos. Laiko žymos grįžta kaip VT_FILETIME UTC laiku. Savos savybės reiškia naudotojo apibrėžtos sekcijos, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, atvėrimą ir jos vardų žodyno apėjimą. Windows aplinkoje visa tai sugeria IPropertyStorage; savo MS-OLEPS analizatoriaus rašymas aplinkai be struktūrinės saugyklos yra tikras projektas, o ne popietės darbas

3 kelias: .xlsx laiko docProps kaip XML zip archyve

Būtent šio kelio dažniausiai ir reikia grandinėms, nes nauji failai jau beveik du dešimtmečius yra .xlsx. OOXML darbo knyga yra zip paketas, o jos savybės pagal paskirtį išskaidytos į mažas dalis: docProps/core.xml laiko Dublin Core laukus dc:title, dc:creator, cp:lastModifiedBy bei dcterms:created ir dcterms:modified kaip W3CDTF laiko žymas UTC laiku, docProps/app.xml laiko programos lygio laukus, tokius kaip Company ir AppVersion, o docProps/custom.xml laiko savas savybes. Kadangi zip centrinis katalogas kiekvieną dalį suranda tiesiogiai, jų skaitymas kainuoja kelis kilobaitus, kad ir kokia didelė būtų darbo knyga. TZipFile ir IXMLDocument, abu esantys pristatomame RTL, atlieka visą darbą

Delphi: xlsx zip paketo išdėstymas, rodantis docProps core, app ir custom XML narius šalia darbalapių dalių, su taisyklėmis, kaip tikrinti nebūtinas dalis ir sugretinti vardų sritis
Darbalapių duomenys užima didžiąją xlsx paketo dalį, tačiau metaduomenys sėdi trijuose mažuose nebūtinuose nariuose šalia jų. Tiesioginė prieiga per zip centrinį katalogą išlaiko skaitymą proporcingą savybėms, o ne darbo knygai
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;

Dvi smulkmenos išlaiko tai patvarų gamyboje. Pirma, dalys yra nebūtinos: minimalus paketas visai be docProps pagal ECMA-376 yra visiškai teisėtas, ir būtent todėl kodas tikrina su IndexOf, o ne daro prielaidą. Antra, elementus gretinkite pagal vietinį vardą ir vardų srities URI, kaip aukščiau daro FindNode, o ne pagal pažodinį priešdėlį; dc: ir cp: yra Excel rašytuvo konvencijos, o kitų generatorių sukurti failai laisvai gali pasirinkti kitokius priešdėlius. Viena aplinkos pastaba: numatytasis IXMLDocument tiekėjas yra MSXML, todėl konsolės programa ar darbinė gija prieš LoadXMLData privalo iškviesti CoInitialize, kitaip pirmoji analizė žus su COM klaida

Kainų lentelė ir kada biblioteka nurungia abu analizatorius

Išmatuota įprastoje kūrėjo mašinoje, COM kelias nusėda ties maždaug dviem–keturiomis sekundėmis vienam failui, kai automatizavimo sesija kuriama kiekvienam failui, ir beveik visa tai yra EXCEL.EXE paleidimas plius pilna darbo knygos analizė, o dar jam reikia įdiegto, licencijuoto Excel visur, kur jis veikia. Du tiesioginiai keliai skaito tik metaduomenų talpyklas, susitvarko per vienaženklį milisekundžių skaičių vienam failui ir jiems nereikia nieko įdiegto, be to, ką Delphi vykdomasis failas jau susieja savyje. Dešimties tūkstančių failų bendrinamame kataloge tai yra skirtumas tarp didžiosios darbo dienos dalies ir mažiau nei minutės, be jokio Office diegimo klausimo

Tiesioginių kelių bėda ta, kad jų yra du. Grandinė, priimanti abu formatus, prižiūri du analizatorius su dviem nepersidengiančiais gedimo būdais: kodų puslapiai ir PROPVARIANT tipai vienoje pusėje, vardų sritys ir nebūtinos dalys kitoje, ir nė vienas neskaito kito formato. Būtent ta priežiūros našta ir yra argumentas už gimtąją biblioteką: HotXLS, losLab Object Pascal skaičiuoklių biblioteka, skirta Delphi ir C++Builder Windows aplinkoje, tuos pačius laukus atskleidžia kaip paprastas darbo knygos savybes – Title, Author, Company, Created ir visas kitas, – kurias užpildo Open tiek .xls, tiek .xlsx failams, be jokio Excel diegimo ir be viso aukščiau aprašyto talpyklų vamzdyno. Savybes ji skaito kaip pilno darbo knygos atvėrimo dalį, o ne kaip vien metaduomenų zondavimą, todėl tinka grandinėms, kurios vis tiek toliau lies langelių duomenis; pilnas savybių paviršius abiejuose fasaduose, įskaitant rašymo pusę, aptartas mūsų straipsnyje apie Excel dokumento savybių nustatymą su HotXLS

Pastaba: pilno Excel analizavimo ir metaduomenų ištraukimo įrankiai yra prieinami HotXLS Delphi VCL komponente