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