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
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
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
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