Be en pipeline att sortera tiotusen kalkylblad efter författare, företag eller senaste ändringsdatum, och det värsta den kan göra är att öppna varje arbetsbok fullt ut. Svaren åker i filens dokumentegenskaper, det som Office-världen kallar Document Summary Information: det metadatalager som Windows Search indexerar, som SharePoint arkiverar efter och som Excel visar i sin egenskapsdialog. Det lagret är på sin höjd några kilobyte, och det ligger på en väldokumenterad plats i båda Excel-formaten. Konststycket är att nå det från Delphi utan att betala för de miljoner celler du inte behöver
Det finns tre verkliga vägar, och de skiljer sig mindre i vad de returnerar än i vad de kräver av maskinen som kör dem. COM-automation styr Excel självt och läser allt, till skrivbordspriser. Formatet .xls håller sina egenskaper i OLE-egenskapsuppsättningsströmmar som Windows tolkar åt dig. Formatet .xlsx håller dem i två små XML-delar inuti en zip som Delphis RTL kan öppna på egen hand. Fungerande kod för var och en följer, med kostnaderna rakt uttalade
Väg 1: COM-automation läser allt, till skrivbordspriser
Automation är den enda vägen med full täckning genom en enda objektmodell: standarduppsättningen med sammanfattningar, den utökade uppsättningen med Company och Manager, och användardefinierade egna egenskaper, alla nåbara genom BuiltinDocumentProperties och CustomDocumentProperties. Allt kommer som en OleVariant, och API:et har en vana värd att känna till innan den biter: en inbyggd egenskap som aldrig tilldelats kommer inte tillbaka tom, den utlöser ett EOleException i samma stund du rör Value. Hjälpfunktionen nedan behandlar det som ”inte satt” snarare än som ett fel
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 finns men tilldelades aldrig
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // skrivskyddad
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å hit på varje väg, annars blir EXCEL.EXE kvar
Excel := Unassigned;
end;
end;
Nu till notan. Excel måste vara installerat på varje maskin där den här koden körs, vilket i sig utesluter de flesta servrar, och Microsofts supportpolicy är uttrycklig med att Office varken är konstruerat eller licensierat för obevakad automation på serversidan. CreateOleObject startar en fullständig EXCEL.EXE och Workbooks.Open tolkar hela arbetsboken, så räkna med ungefär två till fyra sekunder per fil innan den första egenskapen kommer tillbaka. Och try..finally runt Quit är ingen utsmyckning: ett undantag som slinker ut mellan CreateOleObject och Quit lämnar kvar en föräldralös EXCEL.EXE som håller ett lås på filen, osynlig tills nästa körning misslyckas mot den. Att återanvända en Excel-instans över en batch fördelar startkostnaden men koncentrerar risken, eftersom en enda vilsen dialogruta på det dolda skrivbordet stoppar varje fil som köar bakom den
Väg 2: .xls lagrar egenskaper i OLE-egenskapsuppsättningsströmmar
En BIFF8-arbetsbok är en sammansatt OLE-fil, ett filsystem i miniatyr av lagringar och strömmar. Celldatan bor i strömmen Workbook; metadatan bor bredvid den i två egenskapsuppsättningsströmmar vars namn börjar med styrtecknet #5: \005SummaryInformation för de klassiska fälten och \005DocumentSummaryInformation för de utökade och egna. Inuti var och en sitter en binär egenskapsuppsättning i MS-OLEPS-layouten, med sektioner nycklade av en formatidentifierare (FMTID) och egenskaper nycklade av ett heltals-egenskaps-ID. Sammanfattningssektionen är FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, där PIDSI_TITLE är $02 och PIDSI_AUTHOR är $04; Company ($0F) och Manager ($0E) bor i dokumentsammanfattningssektionen, och egna egenskaper i en andra sektion bakom en namnordbok
Den goda nyheten är att du på Windows aldrig tolkar de byten själv. Strukturerad lagring exponerar strömmarna genom IPropertySetStorage, och följande kompilerar som den står mot RTL:ens standardenheter
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: finns inte
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// användning: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Ett ärligt ord om vad kodsnutten döljer. Strängar kan komma som VT_LPWSTR eller som VT_LPSTR, och i ANSI-fallet är byten kodade i egenskapsuppsättningens egen teckentabell, som själv lagras som egenskap 1 i sektionen, så typomvandlingen ovan är bara exakt när den teckentabellen matchar systemets. Tidsstämplar kommer tillbaka som VT_FILETIME i UTC. Egna egenskaper innebär att öppna den användardefinierade sektionen, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, och gå igenom dess namnordbok. IPropertyStorage absorberar allt det på Windows; att skriva en egen MS-OLEPS-tolk för en miljö utan strukturerad lagring är ett riktigt projekt, inte en eftermiddag
Väg 3: .xlsx håller docProps som XML inuti zip-filen
Det här är vägen de flesta pipelines faktiskt behöver, eftersom nya filer varit .xlsx i snart två decennier. En OOXML-arbetsbok är ett zip-paket, och dess egenskaper är uppdelade över små delar efter syfte: docProps/core.xml håller Dublin Core-fälten, dc:title, dc:creator, cp:lastModifiedBy, plus dcterms:created och dcterms:modified som W3CDTF-tidsstämplar i UTC, medan docProps/app.xml håller fält på applikationsnivå såsom Company och AppVersion, och docProps/custom.xml håller egna egenskaper. Eftersom zip-filens centralkatalog lokaliserar varje del direkt kostar en läsning av dem några kilobyte oavsett hur stor arbetsboken är. TZipFile och IXMLDocument, båda i den levererade RTL:en, gör hela jobbet
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;
Två detaljer håller det här robust i produktion. För det första är delarna valfria: ett minimalt paket helt utan docProps är fullt giltigt enligt ECMA-376, vilket är varför koden sonderar med IndexOf istället för att anta. För det andra, matcha element på lokalt namn och namnrymds-URI, som FindNode gör ovan, aldrig på literalt prefix; dc: och cp: är konventioner hos Excels skrivare, och filer framställda av andra generatorer får välja andra prefix. En anmärkning om miljön: standardleverantören för IXMLDocument är MSXML, så en konsolapplikation eller arbetstråd måste anropa CoInitialize före LoadXMLData, annars dör den första tolkningen med ett COM-fel
Kostnadsbladet, och när ett bibliotek slår båda tolkarna
Mätt på en vanlig utvecklarmaskin landar COM-vägen på ungefär två till fyra sekunder per fil när automationssessionen skapas per fil, nästan allt är start av EXCEL.EXE plus en fullständig tolkning av arbetsboken, och den kräver ett installerat, licensierat Excel överallt där den körs. De två direkta vägarna läser enbart metadatabehållarna, blir klara på ensiffriga millisekunder per fil, och behöver inget installerat utöver det en Delphi-körbar fil redan länkar in. Över en filutdelning med tiotusen filer är det skillnaden mellan större delen av en arbetsdag och under en minut, utan någon fråga om Office-utrullning på köpet
Haken med de direkta vägarna är att de är två. En pipeline som tar emot båda formaten underhåller två tolkar med två åtskilda felbeteenden, teckentabeller och PROPVARIANT-typer på ena sidan, namnrymder och valfria delar på den andra, och ingen av dem läser den andras format. Den underhållsbördan är argumentet för ett inbyggt bibliotek: HotXLS, losLabs kalkylbladsbibliotek i Object Pascal för Delphi och C++Builder på Windows, exponerar samma fält som vanliga arbetsboksegenskaper, Title, Author, Company, Created och resten, ifyllda av Open för både .xls och .xlsx, utan någon Excel-installation och utan något av rörmokeriet med behållare ovan. Det läser egenskaper som en del av en fullständig öppning av arbetsboken snarare än en sondering av enbart metadata, så det passar pipelines som ändå ska röra celldatan; hela egenskapsytan på båda fasaderna, inklusive skrivsidan, täcks i vår artikel om att sätta Excel-dokumentegenskaper med HotXLS
Observera: Fullständiga verktyg för Excel-tolkning och metadataextraktion finns i HotXLS Delphi VCL Component