Bir işlem hattından on bin elektronik tabloyu yazara, şirkete ya da son değiştirilme tarihine göre yönlendirmesini isteyin; yapabileceği en kötü şey her çalışma kitabını baştan sona açmaktır. Yanıtlar dosyanın belge özelliklerinde taşınır, yani Office dünyasının Document Summary Information dediği katmanda: Windows Search'ün dizinlediği, SharePoint'in dosyalama ölçütü olarak kullandığı ve Excel'in Özellikler iletişim kutusunda gösterdiği meta veri katmanı. Bu katman en fazla birkaç kilobayttır ve her iki Excel biçiminde de iyi belgelenmiş bir yerde durur. İşin püf noktası, ihtiyacınız olmayan bir milyon hücrenin bedelini ödemeden ona Delphi'den ulaşmaktır
Gerçekte üç yol vardır ve bunlar ne döndürdüklerinden çok, kendilerini çalıştıran makineden ne talep ettikleriyle ayrışır. COM otomasyonu Excel'in kendisini sürer ve her şeyi okur, masaüstü fiyatına. .xls biçimi özelliklerini, Windows'un sizin yerinize ayrıştıracağı OLE özellik kümesi akışlarında tutar. .xlsx biçimi ise onları, Delphi RTL'inin kendi başına açabildiği bir zip içindeki iki küçük XML parçasında tutar. Her biri için çalışan kod, maliyetleri açıkça belirtilmiş hâlde aşağıda
Yol 1: COM otomasyonu her şeyi okur, masaüstü fiyatına
Otomasyon, tek bir nesne modeli üzerinden tam kapsam sunan tek yoldur: standart özet kümesi, Company ve Manager içeren genişletilmiş küme ve kullanıcı tanımlı özel özellikler, hepsine BuiltinDocumentProperties ve CustomDocumentProperties üzerinden erişilir. Her şey OleVariant olarak gelir ve API'nin, ısırmadan önce bilinmeye değer bir alışkanlığı vardır: hiç atanmamış yerleşik bir özellik boş dönmez, Value alanına dokunduğunuz anda bir EOleException yükseltir. Aşağıdaki yardımcı işlev bunu bir başarısızlık olarak değil, "atanmamış" olarak yorumlar
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 := ''; // özellik vardır ama hiç atanmamıştır
end;
end;
begin
Excel := CreateOleObject('Excel.Application');
try
Excel.DisplayAlerts := False;
Book := Excel.Workbooks.Open(FileName, 0, True); // salt okunur
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; // her yolda buraya ulaşın, yoksa EXCEL.EXE arkada kalır
Excel := Unassigned;
end;
end;
Şimdi faturaya gelelim. Bu kodun çalıştığı her makinede Excel kurulu olmalıdır ki bu tek başına sunucuların çoğunu devre dışı bırakır, üstelik Microsoft'un destek politikası Office'in gözetimsiz sunucu tarafı otomasyon için ne tasarlandığını ne de lisanslandığını açıkça söyler. CreateOleObject tam bir EXCEL.EXE başlatır ve Workbooks.Open çalışma kitabının tamamını ayrıştırır; dolayısıyla ilk özellik geri gelmeden önce dosya başına kabaca iki ila dört saniye bekleyin. Quit çevresindeki try..finally de süs değildir: CreateOleObject ile Quit arasında kaçan bir istisna, dosya üzerinde kilit tutan öksüz bir EXCEL.EXE bırakır ve bu, bir sonraki çalıştırma ona takılana kadar görünmez kalır. Bir toplu iş boyunca tek bir Excel örneğini yeniden kullanmak başlatma maliyetini amorti eder ama riski yoğunlaştırır, çünkü gizli masaüstünde beliren tek bir başıboş iletişim kutusu, arkasında kuyruğa girmiş her dosyayı durdurur
Yol 2: .xls özellikleri OLE özellik kümesi akışlarında saklar
Bir BIFF8 çalışma kitabı, depolamalardan ve akışlardan oluşan minyatür bir dosya sistemi olan OLE bileşik dosyasıdır. Hücre verisi Workbook akışında yaşar; meta veri ise onun yanında, adları #5 denetim karakteriyle başlayan iki özellik kümesi akışında durur: klasik alanlar için \005SummaryInformation, genişletilmiş ve özel alanlar için \005DocumentSummaryInformation. Her birinin içinde MS-OLEPS düzeninde ikili bir özellik kümesi bulunur; bölümler bir biçim tanımlayıcısıyla (FMTID), özellikler ise tamsayı bir özellik kimliğiyle anahtarlanır. Özet bölümünün FMTID değeri {F29F85E0-4FF9-1068-AB91-08002B27B3D9} olup burada PIDSI_TITLE $02, PIDSI_AUTHOR ise $04 değerini taşır; Company ($0F) ve Manager ($0E) belge özeti bölümünde, özel özellikler ise bir ad sözlüğünün ardındaki ikinci bir bölümde yer alır
İyi haber şu ki Windows'ta o baytları asla kendiniz ayrıştırmazsınız. Yapılandırılmış depolama, akışları IPropertySetStorage aracılığıyla açığa çıkarır ve aşağıdaki kod, standart RTL birimlerine karşı gösterildiği gibi derlenir
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: mevcut değil
try
case Value.vt of
VT_LPSTR: Result := string(AnsiString(Value.pszVal));
VT_LPWSTR: Result := Value.pwszVal;
end;
finally
PropVariantClear(Value);
end;
end;
// kullanım: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));
Bu parçacığın gizlediği şeyler hakkında dürüst bir not. Dizeler VT_LPWSTR ya da VT_LPSTR olarak gelebilir ve ANSI durumunda baytlar, bölümün 1 numaralı özelliğinde saklanan kendi kod sayfasıyla kodlanmıştır; dolayısıyla yukarıdaki dönüşüm yalnızca o kod sayfası sistemin kod sayfasıyla eşleştiğinde kesindir. Zaman damgaları UTC olarak VT_FILETIME biçiminde döner. Özel özellikler, kullanıcı tanımlı bölümü, yani FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE} bölümünü açmayı ve ad sözlüğünde dolaşmayı gerektirir. IPropertyStorage Windows'ta bunların hepsini üstlenir; yapılandırılmış depolamanın bulunmadığı bir ortam için kendi MS-OLEPS ayrıştırıcınızı yazmak bir öğleden sonralık iş değil, gerçek bir projedir
Yol 3: .xlsx docProps parçalarını zip içinde XML olarak tutar
Yeni dosyalar neredeyse yirmi yıldır .xlsx olduğundan, işlem hatlarının gerçekten ihtiyaç duyduğu yol budur. Bir OOXML çalışma kitabı zip paketidir ve özellikleri amaca göre küçük parçalara bölünmüştür: docProps/core.xml Dublin Core alanlarını, yani dc:title, dc:creator, cp:lastModifiedBy alanlarını ve UTC W3CDTF zaman damgaları olarak dcterms:created ile dcterms:modified alanlarını tutar; docProps/app.xml Company ve AppVersion gibi uygulama düzeyindeki alanları, docProps/custom.xml ise özel özellikleri tutar. Zip'in merkezi dizini her parçayı doğrudan konumlandırdığı için, çalışma kitabı ne kadar büyük olursa olsun bunları okumak yalnızca birkaç kilobayta mal olur. Her ikisi de dağıtılan RTL içinde bulunan TZipFile ve IXMLDocument işin tamamını görür
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;
İki ayrıntı bunu üretimde sağlam tutar. Birincisi, parçalar isteğe bağlıdır: hiç docProps içermeyen asgari bir paket ECMA-376 altında tümüyle geçerlidir ve kodun varsayım yapmak yerine IndexOf ile yoklamasının nedeni budur. İkincisi, öğeleri yukarıda FindNode işlevinin yaptığı gibi yerel ada ve ad alanı URI değerine göre eşleştirin, asla düz öneke göre değil; dc: ve cp: Excel yazıcısının alışkanlıklarıdır ve başka üreticilerin ürettiği dosyalar farklı önekler seçmekte serbesttir. Ortama dair bir not: varsayılan IXMLDocument sağlayıcısı MSXML olduğundan, bir konsol uygulaması ya da çalışan iş parçacığı LoadXMLData çağrısından önce CoInitialize çağırmalıdır, yoksa ilk ayrıştırma bir COM hatasıyla ölür
Maliyet tablosu ve bir kütüphanenin iki ayrıştırıcıyı da geçtiği an
Sıradan bir geliştirici makinesinde ölçüldüğünde, otomasyon oturumu dosya başına oluşturulduğunda COM yolu dosya başına kabaca iki ila dört saniyeye oturur; bunun neredeyse tamamı EXCEL.EXE başlatma süresi ile tam bir çalışma kitabı ayrıştırmasıdır ve çalıştığı her yerde kurulu, lisanslı bir Excel gerektirir. İki doğrudan yol yalnızca meta veri kaplarını okur, dosya başına tek haneli milisaniyelerde biter ve bir Delphi yürütülebilirinin zaten bağladıklarının ötesinde hiçbir şeyin kurulu olmasını gerektirmez. On bin dosyalık bir paylaşımda bu, bir iş gününün büyük kısmı ile bir dakikanın altı arasındaki farktır, üstelik hiçbir Office dağıtım sorusu eklenmeden
Doğrudan yolların püf noktası, iki tane olmalarıdır. Her iki biçimi de kabul eden bir işlem hattı, ayrık iki başarısızlık kipiyle iki ayrıştırıcı bakar; bir yanda kod sayfaları ve PROPVARIANT türleri, öbür yanda ad alanları ve isteğe bağlı parçalar vardır ve hiçbiri diğerinin biçimini okumaz. Bu bakım yükü, yerel bir kütüphanenin gerekçesidir: losLab'in Windows üzerinde Delphi ve C++Builder için Object Pascal elektronik tablo kütüphanesi olan HotXLS, aynı alanları düz çalışma kitabı özellikleri olarak, yani Title, Author, Company, Created ve gerisi olarak açığa çıkarır; bunlar .xls ve .xlsx için aynı biçimde Open tarafından doldurulur, Excel kurulumu ve yukarıdaki kap tesisatının hiçbiri olmadan. Özellikleri, yalnızca meta veriyi yoklayan bir işlem yerine tam bir çalışma kitabı açılışının parçası olarak okur, bu yüzden nasılsa hücre verisine de dokunacak işlem hatlarına uyar; her iki cephedeki tam özellik yüzeyi, yazma tarafı dahil, HotXLS ile Excel belge özelliklerini ayarlama üzerine yazımızda ele alınmaktadır
Not: Eksiksiz Excel ayrıştırma ve meta veri çıkarma araçları HotXLS Delphi VCL Bileşeni içinde bulunur