Teknik Makale

Delphi'de Excel Belge Özelliklerini Okuma: Üç Yol

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

Excel Document Summary Information verisine ulaşan üç Delphi yolunun şeması: Excel'in kendisini süren COM otomasyonu, xls dosyaları için OLE özellik kümesi akışları ve xlsx paketleri için OOXML docProps XML ayrıştırma
COM otomasyonu, lisanslı bir masaüstü Excel ve dosya başına saniyeler karşılığında tam kapsam satın alır; iki biçim-yerel yol ise yalnızca meta veri kaplarını milisaniyeler içinde okur. Her yolun döndürdüğü şey neredeyse aynıdır — ana makineden istediği şey ise değil

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

Workbook akışını SummaryInformation ve DocumentSummaryInformation özellik kümelerinin yanına yerleştiren BIFF8 xls bileşik dosyasının Delphi anatomisi ve StgOpenStorageEx ile IPropertySetStorage arasındaki erişim zinciri
Bir xls dosyası hücre verisini ve belge özelliklerini bir OLE bileşik dosyasında kardeş akışlar olarak saklar. Windows ikili özellik kümelerini sizin yerinize ayrıştırır, böylece Delphi kodu ne MS-OLEPS düzenlerine ne de kod sayfalarına elle dokunur

İ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

Delphi: Çalışma sayfası parçalarının yanında docProps core, app ve custom XML üyelerini gösteren bir xlsx zip paketinin düzeni ve isteğe bağlı parçaları yoklama ile ad alanlarını eşleştirmeye dair üretim kuralları
Bir xlsx paketine çalışma sayfası verisi hâkimdir, yine de meta veri onun yanında üç küçük isteğe bağlı üyede durur. Zip merkezi dizini üzerinden rastgele erişim, okumayı çalışma kitabıyla değil özelliklerle orantılı tutar
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