Actualizarea compară automatizarea COM cu fluxurile OLE și OOXML și explică atunci când extragerea dedicată cu HotXLS evită costul și fragilitatea celor două parsere

Articol tehnic

Extragerea informațiilor de sumar ale documentului din fișiere Excel în Delphi

Atunci când procesați loturi mari de foi de calcul Excel într-un flux automatizat, rareori doriți să încărcați întregul document în memorie doar pentru a afla ce este. Adesea, metadatele încorporate în fișier (autorul, titlul, data creării și proprietățile personalizate) sunt suficiente pentru a direcționa, indexa sau respinge documentul. În lumea Microsoft Office, aceste metadate sunt cunoscute sub numele de Document Summary Information (Informații de sumar ale documentului)

Extragerea acestor informații nativ în Delphi fără a vă baza pe automatizarea OLE (care necesită ca Excel să fie instalat pe mașina gazdă) necesită parsarea directă a structurii fișierului de bază. În acest articol, vom analiza modul în care funcționează sumarele de documente în fișierele Excel și cum să le extragem eficient utilizând parsarea directă a fluxurilor

Înțelegerea fluxurilor de metadate Excel

Din punct de vedere istoric, fișierele Excel mai vechi (.xls) sunt stocate în formate OLE Compound Document, acționând efectiv ca niște mini-sisteme de fișiere care conțin fluxuri și zone de stocare. Metadatele sunt găzduite în două fluxuri specifice:

  • SummaryInformation: Conține proprietăți standard precum Title, Subject, Author, Keywords și Revision Number
  • DocumentSummaryInformation: Conține proprietăți extinse, cum ar fi Company, Manager și proprietăți personalizate definite de utilizator

Fișierele Excel moderne (.xlsx) utilizează formatul Office Open XML (OOXML), care este o structură XML arhivată tip ZIP. Metadatele aici se află în docProps/core.xml, docProps/app.xml și docProps/custom.xml. O componentă robustă de parsare Delphi trebuie să gestioneze impecabil ambele structuri interne, expunând în același timp un API unificat dezvoltatorului

Parsarea documentelor compuse OLE în Delphi

Pentru a citi informațiile de sumar dintr-un fișier `.xls` moștenit fără instrumente de la terți, trebuie să parsați stocarea structurată OLE (OLE Structured Storage). Microsoft expune acest lucru prin interfața COM IPropertySetStorage. Iată o implementare Delphi de bază care evită pornirea Excel:

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;

Extragerea programatică cu HotXLS

Deși API-ul Windows COM funcționează pentru fișierele `.xls`, nu funcționează pentru fișierele moderne `.xlsx` (care sunt arhive ZIP). Mai mult, utilizarea API-ului COM pe mai multe platforme (de exemplu, pe Linux sau macOS prin FireMonkey) este imposibilă. Actualizările recente ale componentei HotXLS au introdus unități dedicate (de exemplu, lxXlsSummary) pentru a izola și a optimiza citirea acestor fluxuri de sumar în ambele formate, complet nativ în cod Delphi

Un exemplu multiplatformă

Utilizând interfețele XlsReadDocumentSummaryInformation și XlsReadSummaryInformation, puteți prelua rapid metadatele tip șir din ambele formate, `.xls` și `.xlsx`, fără să vă faceți griji cu privire la arhitectura sistemului de fișiere subiacent

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));

De ce contează extragerea dedicată a sumarului

Beneficiul principal al acestei abordări este performanța și siguranța memoriei. Evitând instanțierea întregului DOM (Document Object Model) al registrului de lucru și parsând doar docProps/core.xml sau fluxurile de proprietăți OLE, amprenta aplicației dumneavoastră rămâne incredibil de mică. Dacă indexați 10.000 de fișiere Excel într-o rețea partajată, încercarea de a parsa complet fiecare fișier vă va epuiza memoria și va dura ore întregi. Extragerea dedicată a sumarului finalizează aceeași sarcină în câteva secunde

Mai mult, citirea nativă a fluxurilor asigură faptul că aplicația dumneavoastră poate rula ca un serviciu de fundal sau pe un server Linux fără interfață grafică (headless), fără a apela vreodată Excel.exe, o cerință critică pentru arhitecturile scalabile moderne

Notă: Instrumente cuprinzătoare pentru parsarea Excel și extragerea metadatelor sunt disponibile în HotXLS VCL Component

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;