Помолете даден конвейер (pipeline) да маршрутизира десет хиляди електронни таблици по автор, компания или дата на последна промяна и най-лошото, което може да направи, е да отвори напълно всяка работна книга (workbook). Отговорите се намират в свойствата на документа на файла, това което светът на Office нарича Обобщена информация за документа (Document Summary Information): слоят с метаданни, който Windows Search индексира, по който SharePoint сортира файловете и който Excel показва в своя диалог Свойства. Този слой е най-много килобайти и се намира на добре документирано място и в двата формата на Excel. Номерът е да го достигнете от Delphi, без да плащате за милиона клетки, от които не се нуждаете
Има три реални маршрута и те се различават по-малко в това, което връщат, отколкото в това, което изискват от машината, която ги изпълнява. COM автоматизацията управлява самия Excel и чете всичко на десктоп цени. Форматът .xls съхранява своите свойства в OLE потоци (property-set streams), които Windows ще парсира вместо вас. Форматът .xlsx ги съхранява в две малки XML части вътре в zip, които Delphi RTL може да отвори самостоятелно. По-долу следва работещ код за всеки от тях, като разходите са посочени ясно
Маршрут 1: COM автоматизацията чете всичко на десктоп цени
Автоматизацията е единственият маршрут с пълно покритие чрез един обектен модел: стандартният обобщаващ набор, разширеният набор с Company и Manager и потребителски дефинирани (custom) свойства, всички достъпни чрез BuiltinDocumentProperties и CustomDocumentProperties. Всичко пристига като OleVariant и API-то има един навик, който си струва да знаете, преди да ви ухапе: вградено свойство, което никога не е било присвоено, не се връща празно, то повдига EOleException в момента, в който докоснете Value. Помощникът (helper) по-долу третира това като "не е зададено", а не като неуспех
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 := ''; // property exists but was never assigned
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // read-only
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; // reach this on every path, or EXCEL.EXE stays behind
Excel := Unassigned;
end;
end;
Сега сметката. Excel трябва да бъде инсталиран на всяка машина, на която се изпълнява този код, което само по себе си изключва повечето сървъри, а политиката за поддръжка на Microsoft изрично гласи, че Office не е проектиран, нито лицензиран за сървърна автоматизация без надзор (unattended). CreateOleObject стартира пълен EXCEL.EXE, а Workbooks.Open парсира цялата работна книга, така че очаквайте приблизително две до четири секунди на файл, преди да се върне първото свойство. И try..finally около Quit не е декорация: изключение, което избяга между CreateOleObject и Quit, оставя сирак EXCEL.EXE, държащ заключване върху файла, невидимо докато следващото изпълнение не се провали срещу него. Повторното използване на един екземпляр (instance) на Excel в партида (batch) амортизира разходите за стартиране, но концентрира риска, защото един заблуден диалог на скрития десктоп спира всеки файл, нареден на опашка зад него
Маршрут 2: .xls съхранява свойствата в OLE property-set потоци
Работната книга BIFF8 е OLE съставен файл (compound file), миниатюрна файлова система от хранилища и потоци. Данните за клетките живеят в потока Workbook; метаданните живеят до тях в два потока за набори от свойства (property-set streams), чиито имена започват с контролния символ #5: \005SummaryInformation за класическите полета и \005DocumentSummaryInformation за разширените и персонализираните. Вътре във всеки седи двоичен набор от свойства в оформлението MS-OLEPS, с секции с ключове (keyed) по идентификатор на формат (FMTID) и свойства с ключове по цяло число за ID на свойство. Обобщаващата секция (summary) е FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}, където PIDSI_TITLE е $02, а PIDSI_AUTHOR е $04; Company ($0F) и Manager ($0E) живеят в секцията за обобщение на документа (document-summary), а персонализираните свойства във втора секция зад речник с имена
Добрата новина е, че в Windows никога не парсирате тези байтове сами. Структурираното съхранение (Structured storage) излага потоците чрез IPropertySetStorage и следното се компилира както е показано срещу стандартните RTL единици
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: not present
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// usage: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Честна дума за това какво крие фрагментът. Низовете могат да пристигнат като VT_LPWSTR или като VT_LPSTR, и в ANSI случая байтовете са кодирани в собствената кодова страница на набора от свойства, която сама по себе си се съхранява като свойство 1 на секцията, така че преобразуването (cast) по-горе е точно само когато тази кодова страница съвпада със системната. Времевите марки (timestamps) се връщат като VT_FILETIME в UTC. Персонализираните свойства означават отваряне на дефинираната от потребителя секция, FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}, и обхождане на нейния речник с имена. IPropertyStorage абсорбира всичко това в Windows; написването на собствен MS-OLEPS парсер за среда без структурирано съхранение е истински проект, а не работа за един следобед
Маршрут 3: .xlsx запазва docProps като XML вътре в zip
Това е маршрутът, от който повечето конвейери всъщност се нуждаят, тъй като новите файлове са .xlsx от близо две десетилетия. Работната книга на OOXML е zip пакет и нейните свойства са разделени в малки части според предназначението: docProps/core.xml съдържа полетата на Dublin Core, dc:title, dc:creator, cp:lastModifiedBy, плюс dcterms:created и dcterms:modified като W3CDTF времеви марки в UTC, докато docProps/app.xml държи полета на ниво приложение като Company и AppVersion, а docProps/custom.xml съдържа персонализирани свойства. Тъй като централната директория на zip-а локализира всяка част директно, прочитането им струва няколко килобайта, без значение колко голяма е работната книга. TZipFile и IXMLDocument, и двете в доставения RTL, вършат цялата работа
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;
Два детайла поддържат това стабилно в продукция. Първо, частите са незадължителни: минимален пакет без никакъв docProps е напълно валиден според ECMA-376, поради което кодът проверява с IndexOf, вместо да предполага. Второ, съпоставяйте елементите по локално име и URI на пространство от имена (namespace), както прави FindNode по-горе, никога по буквален префикс; dc: и cp: са конвенции на писателя на Excel, а файлове, създадени от други генератори, са свободни да избират различни префикси. Една бележка за средата: доставчикът по подразбиране на IXMLDocument е MSXML, така че конзолно приложение или работна нишка трябва да извика CoInitialize преди LoadXMLData, в противен случай първият опит за парсиране умира с COM грешка
Листът с разходите и кога една библиотека побеждава и двата парсера
Измерен на обикновена машина на разработчик, COM маршрутът достига приблизително до две до четири секунди на файл, когато сесията за автоматизация се създава за всеки файл, почти цялото от което е стартиране на EXCEL.EXE плюс пълно парсиране на работната книга, и изисква инсталиран, лицензиран Excel където и да работи. Двата директни маршрута четат само контейнерите с метаданни, приключват за едноцифрени милисекунди на файл и не се нуждаят от нищо инсталирано отвъд това, което един изпълним файл (executable) на Delphi вече свързва. При дял от десет хиляди файла, това е разликата между по-голямата част от работния ден и по-малко от минута, без да е свързан въпрос с внедряването на Office
Уловката при директните маршрути е, че те са два. Конвейер, който приема и двата формата, поддържа два парсера с два разделени режима на повреда, кодови страници и PROPVARIANT типове от едната страна, пространства от имена (namespaces) и незадължителни части от другата, и нито един от тях не чете формата на другия. Това натоварване по поддръжката е случаят с нативна библиотека: HotXLS, библиотеката на losLab за електронни таблици на Object Pascal за Delphi и C++Builder в Windows, излага същите полета като обикновени свойства на работната книга (Title, Author, Company, Created и останалите), попълнени от Open както за .xls, така и за .xlsx, без инсталация на Excel и без нищо от водопроводната инфраструктура на контейнерите по-горе. Тя чете свойствата като част от пълно отваряне на работната книга, а не като сондаж само за метаданни, така че пасва на конвейери, които така или иначе продължават да докосват данните в клетките; пълната повърхност за свойства на двете фасади, включително страната за запис, е обхваната в нашата статия относно задаването на свойства на Excel документ с HotXLS
Забележка: Инструментите за пълно парсиране на Excel и извличане на метаданни са налични в компонента HotXLS VCL