Teknisk artikkel

Lese Excel-dokumentegenskaper i Delphi: tre veier

Be en løype om å rute ti tusen regneark etter forfatter, firma eller dato for siste endring, og det verste den kan gjøre er å åpne hver arbeidsbok fullt ut. Svarene ligger i filens dokumentegenskaper, det Office-verdenen kaller Document Summary Information: metadatalaget som Windows Search indekserer, som SharePoint arkiverer etter, og som Excel viser i egenskapsdialogen sin. Det laget er på høyden noen kilobyte, og det ligger på et godt dokumentert sted i begge Excel-formatene. Kunsten er å nå det fra Delphi uten å betale for den millionen celler du ikke trenger

Det finnes tre reelle veier, og de skiller seg mindre i hva de gir tilbake enn i hva de krever av maskinen som kjører dem. COM-automatisering driver Excel selv og leser alt, til skrivebordspriser. Formatet .xls holder egenskapene sine i OLE-egenskapssettstrømmer som Windows tolker for deg. Formatet .xlsx holder dem i to små XML-deler inne i en zip som Delphi-RTL-en kan åpne på egen hånd. Fungerende kode for hver av dem følger, med kostnadene sagt rett ut

Diagram over tre Delphi-veier til Excel Document Summary Information: COM-automatisering som driver Excel selv, OLE-egenskapssettstrømmer for xls-filer, og tolking av docProps-XML i OOXML for xlsx-pakker
COM-automatisering kjøper full dekning til prisen av et lisensiert Excel på skrivebordet og sekunder per fil, mens de to formatnære veiene bare leser metadatabeholdere på millisekunder. Det hver vei gir tilbake er nesten det samme — det den krever av vertsmaskinen er det ikke

Vei 1: COM-automatisering leser alt, til skrivebordspriser

Automatisering er den eneste veien med full dekning gjennom én objektmodell: standardsettet med sammendragsfelt, det utvidede settet med Company og Manager, og brukerdefinerte egenskaper, alle tilgjengelige gjennom BuiltinDocumentProperties og CustomDocumentProperties. Alt kommer som en OleVariant, og API-et har én vane det lønner seg å kjenne før den biter: en innebygd egenskap som aldri ble tildelt en verdi, kommer ikke tilbake tom, den utløser en EOleException i det øyeblikket du rører Value. Hjelperen nedenfor behandler det som "ikke satt" i stedet for som en feil

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 := '';   // egenskapen finnes, men ble aldri tildelt en verdi
    end;
  end;

begin
  Excel := CreateOleObject('Excel.Application');
  try
    Excel.DisplayAlerts := False;
    Book := Excel.Workbooks.Open(FileName, 0, True);   // skrivebeskyttet
    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;   // nås på hver vei ut, ellers blir EXCEL.EXE liggende igjen
    Excel := Unassigned;
  end;
end;

Så til regningen. Excel må være installert på hver maskin denne koden kjører på, noe som i seg selv utelukker de fleste servere, og Microsofts støttepolicy er tydelig på at Office verken er laget for eller lisensiert til automatisering på serversiden uten tilsyn. CreateOleObject starter en full EXCEL.EXE og Workbooks.Open tolker hele arbeidsboken, så regn med rundt to til fire sekunder per fil før den første egenskapen kommer tilbake. Og try..finally rundt Quit er ikke pynt: et unntak som slipper ut mellom CreateOleObject og Quit etterlater en foreldreløs EXCEL.EXE som holder en lås på filen, usynlig helt til neste kjøring feiler mot den. Å gjenbruke én Excel-instans gjennom en hel bunke amortiserer oppstartskostnaden, men konsentrerer risikoen, for én vilfaren dialog på det skjulte skrivebordet stanser hver fil som står i kø bak den

Vei 2: .xls lagrer egenskaper i OLE-egenskapssettstrømmer

En BIFF8-arbeidsbok er en sammensatt OLE-fil, et miniatyrfilsystem av lagre og strømmer. Celledataene ligger i Workbook-strømmen; metadataene ligger ved siden av i to egenskapssettstrømmer med navn som begynner med kontrolltegnet #5: \005SummaryInformation for de klassiske feltene og \005DocumentSummaryInformation for de utvidede og de egendefinerte. Inne i hver av dem ligger et binært egenskapssett i MS-OLEPS-oppsettet, med seksjoner nøklet på en formatidentifikator (FMTID) og egenskaper nøklet på en heltalls egenskaps-ID. Sammendragsseksjonen er FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, der PIDSI_TITLE er $02 og PIDSI_AUTHOR er $04; Company ($0F) og Manager ($0E) ligger i dokumentsammendragsseksjonen, og egendefinerte egenskaper i en andre seksjon bak en navneordbok

Delphi: Anatomien til en sammensatt BIFF8-xls-fil som plasserer Workbook-strømmen ved siden av egenskapssettene SummaryInformation og DocumentSummaryInformation, med tilgangskjeden fra StgOpenStorageEx til IPropertySetStorage
En xls-fil lagrer celledata og dokumentegenskaper som søskenstrømmer i en sammensatt OLE-fil. Windows tolker de binære egenskapssettene for deg, så Delphi-kode rører verken MS-OLEPS-oppsett eller kodesider for hånd

Den gode nyheten er at på Windows tolker du aldri de bytene selv. Strukturert lagring eksponerer strømmene gjennom IPropertySetStorage, og det følgende kompilerer som det står mot RTL-unitene som følger med

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

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

Et ærlig ord om hva utdraget skjuler. Strenger kan komme som VT_LPWSTR eller som VT_LPSTR, og i ANSI-tilfellet er bytene kodet i egenskapssettets egen kodeside, som selv er lagret som egenskap 1 i seksjonen, så typekonverteringen over er bare eksakt når den kodesiden stemmer med systemets. Tidsstempler kommer tilbake som VT_FILETIME i UTC. Egendefinerte egenskaper betyr å åpne den brukerdefinerte seksjonen, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, og gå gjennom navneordboken dens. IPropertyStorage tar seg av alt dette på Windows; å skrive din egen MS-OLEPS-tolker for et miljø uten strukturert lagring er et ekte prosjekt, ikke en ettermiddag

Vei 3: .xlsx holder docProps som XML inne i zip-en

Dette er veien de fleste løyper faktisk trenger, siden nye filer har vært .xlsx i snart to tiår. En OOXML-arbeidsbok er en zip-pakke, og egenskapene er fordelt på små deler etter formål: docProps/core.xml holder Dublin Core-feltene, dc:title, dc:creator, cp:lastModifiedBy, pluss dcterms:created og dcterms:modified som W3CDTF-tidsstempler i UTC, mens docProps/app.xml holder felt på applikasjonsnivå som Company og AppVersion, og docProps/custom.xml holder egendefinerte egenskaper. Fordi zip-ens sentralkatalog finner hver del direkte, koster det å lese dem noen få kilobyte uansett hvor stor arbeidsboken er. TZipFile og IXMLDocument, begge i RTL-en som følger med, gjør hele jobben

Delphi: Oppsettet i en xlsx-zip-pakke som viser XML-medlemmene docProps core, app og custom ved siden av regnearkdelene, med produksjonsreglene for å sondere valgfrie deler og matche navnerom
Regnearkdataene dominerer en xlsx-pakke, men metadataene ligger i tre små og valgfrie medlemmer ved siden av. Direkte oppslag via zip-ens sentralkatalog holder lesingen proporsjonal med egenskapene, ikke med arbeidsboken
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;

To detaljer holder dette robust i produksjon. For det første er delene valgfrie: en minimal pakke helt uten docProps er fullt gyldig under ECMA-376, og det er derfor koden sonderer med IndexOf i stedet for å anta. For det andre må du matche elementer på lokalt navn og navneroms-URI, slik FindNode gjør over, aldri på bokstavelig prefiks; dc: og cp: er konvensjoner fra skriveren i Excel, og filer laget av andre generatorer står fritt til å velge andre prefikser. En merknad om miljøet: standardleverandøren for IXMLDocument er MSXML, så et konsollprogram eller en arbeidertråd må kalle CoInitialize før LoadXMLData, ellers dør den første tolkingen med en COM-feil

Kostnadsoppstillingen, og når et bibliotek slår begge tolkerne

Målt på en vanlig utviklermaskin havner COM-veien på rundt to til fire sekunder per fil når automatiseringsøkten opprettes per fil, nesten alt av det oppstart av EXCEL.EXE pluss en full tolking av arbeidsboken, og den krever et installert og lisensiert Excel overalt der den kjører. De to direkte veiene leser bare metadatabeholderne, blir ferdige på ensifrede millisekunder per fil, og trenger ingenting installert utover det en kjørbar Delphi-fil allerede lenker inn. På et delt område med ti tusen filer er det forskjellen mellom mesteparten av en arbeidsdag og under ett minutt, uten noe spørsmål om Office-utrulling knyttet til seg

Haken ved de direkte veiene er at det er to av dem. En løype som tar imot begge formatene, vedlikeholder to tolkere med to helt ulike feilmoduser, kodesider og PROPVARIANT-typer på den ene siden, navnerom og valgfrie deler på den andre, og ingen av dem leser den andres format. Den vedlikeholdsbyrden er argumentet for et innfødt bibliotek: HotXLS, losLabs Object Pascal-regnearkbibliotek for Delphi og C++Builder på Windows, eksponerer de samme feltene som vanlige arbeidsbokegenskaper, Title, Author, Company, Created og resten, fylt av Open for både .xls og .xlsx, uten noen Excel-installasjon og uten noe av beholderrøret over. Det leser egenskaper som del av en full åpning av arbeidsboken i stedet for en sondering som bare henter metadata, så det passer løyper som uansett skal videre til celledataene; hele egenskapsflaten på begge fasadene, inkludert skrivesiden, er dekket i artikkelen vår om å sette Excel-dokumentegenskaper med HotXLS

Merk: Fullstendige verktøy for Excel-tolking og uttrekk av metadata er tilgjengelige i HotXLS Delphi VCL Component