技術記事

DelphiでのExcelファイルからのドキュメントプロパティ情報の抽出

自動化されたパイプラインで大量のExcelスプレッドシートのバッチを処理する場合、その内容を把握するためだけにドキュメント全体をメモリにロードすることはめったにありません。多くの場合、ファイル内に埋め込まれたメタデータ(作成者、タイトル、作成日、カスタムプロパティなど)だけで、ドキュメントのルーティング、インデックス作成、または拒否を行うのに十分です。Microsoft Officeの世界では、このメタデータはドキュメントプロパティ情報(Document Summary Information)として知られています

DelphiでホストマシンにExcelをインストールせず、OLEオートメーションにも依存せずにこの情報をネイティブ抽出するには、基盤となるファイル構造を直接解析する必要があります。この記事では、Excelファイルでドキュメントプロパティがどのように機能するか、および生のストリーム解析を使用してそれらを効率的に抽出する方法について説明します

経路1:COMオートメーションでドキュメントプロパティを読む

歴史的に、古いExcelファイル(.xls)はOLE複合ドキュメント形式(OLE Compound Document)で保存されており、ストリームとストレージを含むミニファイルシステムとして効果的に機能します。メタデータは、次の2つの特定のストリームに格納されています

  • SummaryInformation: タイトル、サブジェクト、作成者、キーワード、リビジョン番号などの標準プロパティが含まれます
  • DocumentSummaryInformation: 会社、管理者、カスタムのユーザー定義プロパティなどの拡張プロパティが含まれます

最新のExcelファイル(.xlsx)は、zip圧縮されたXML構造であるOffice Open XML(OOXML)形式を使用しています。ここでのメタデータは、docProps/core.xmldocProps/app.xml、およびdocProps/custom.xmlにあります。堅牢なDelphi解析コンポーネントは、開発者に統一されたAPIを公開しながら、両方の内部構造をシームレスに処理する必要があります

経路2:.xlsのOLEプロパティセットストリーム

サードパーティのツールなしでレガシーな「.xls」ファイルからSummaryInformationを読み取るには、OLE構造化ストレージを解析する必要があります。MicrosoftはこれをCOMインターフェースIPropertySetStorageを通じて公開しています。以下は、Excelを起動せずに済む生のDelphiの実装です

uses
  System.SysUtils, System.Win.ComObj, Winapi.ActiveX, Winapi.Windows;

procedure ExtractXlsSummaryInfo(const FileName: string);
var
  Stg: IStorage;
  PropSetStg: IPropertySetStorage;
  PropStg: IPropertyStorage;
  PropSpec: TPropSpec;
  PropVariant: TPropVariant;
  Hr: HRESULT;
begin
  // Open the OLE Compound Document
  Hr := StgOpenStorage(PWideChar(WideString(FileName)), nil,
    STGM_READ or STGM_SHARE_DENY_WRITE, nil, 0, Stg);
    
  if Failed(Hr) then
    raise Exception.Create('Failed to open OLE storage. File may not be a valid .xls document.');

  // Query for the property set storage interface
  if Stg.QueryInterface(IPropertySetStorage, PropSetStg) = S_OK then
  begin
    // Open the SummaryInformation stream (FMTID_SummaryInformation)
    Hr := PropSetStg.Open(FMTID_SummaryInformation, STGM_READ or STGM_SHARE_EXCLUSIVE, PropStg);
    if Succeeded(Hr) then
    begin
      // Read the Author property (PIDSI_AUTHOR = 4)
      PropSpec.ulKind := PRSPEC_PROPID;
      PropSpec.propid := PIDSI_AUTHOR;
      
      if PropStg.ReadMultiple(1, @PropSpec, @PropVariant) = S_OK then
      begin
        if PropVariant.vt = VT_LPSTR then
          Writeln('Author: ', string(AnsiString(PropVariant.pszVal)));
        PropVariantClear(PropVariant);
      end;
    end;
  end;
end;

経路3:.xlsxのdocProps XML

Windows COM APIは「.xls」ファイルには機能しますが、(ZIPアーカイブである)最新の「.xlsx」ファイルには機能しません。さらに、クロスプラットフォーム(たとえば、FireMonkey経由でLinuxまたはmacOS)でCOM APIを使用することは不可能です。HotXLSコンポーネントの最近のアップデートでは、Delphiコードで完全にネイティブに両方の形式にわたってこれらのプロパティストリームの読み取りを分離および最適化するための専用ユニット(lxXlsSummaryなど)が導入されました

XlsReadDocumentSummaryInformationおよびXlsReadSummaryInformationインターフェースを使用すると、基盤となるファイルシステムアーキテクチャを気にすることなく、「.xls」と「.xlsx」の両方からメタデータ文字列をすばやく取得できます

uses
  lxXlsSummary;

var
  Summary: TXlsSummaryInfo;
  ExtendedInfo: TXlsDocumentSummaryInfo;
begin
  // Extract standard summary from an OOXML format seamlessly
  Summary := XlsReadSummaryInformation('C:\\Data\\FinancialReport.xlsx');
  try
    Writeln('Title: ', Summary.Title);
    Writeln('Author: ', Summary.Author);
    Writeln('Creation Date: ', DateTimeToStr(Summary.CreateTime));
  finally
    Summary.Free;
  end;

  // Extract extended document summary
  ExtendedInfo := XlsReadDocumentSummaryInformation('C:\\Data\\FinancialReport.xlsx');
  try
    Writeln('Company: ', ExtendedInfo.Company);
    Writeln('Manager: ', ExtendedInfo.Manager);
  finally
    ExtendedInfo.Free;
  end;
end;

.xlsxではdocProps/core.xmlにタイトル、作成者、最終更新者、UTCの作成・変更時刻があり、docProps/app.xmlにはCompanyとAppVersionがあります。docProps/custom.xmlのカスタムプロパティも含め、ZIPの中央ディレクトリから必要なパーツだけを読み取れます

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;

パーツは任意なのでIndexOfで存在を確認し、XMLの接頭辞ではなくローカル名と名前空間URIで照合します。既定のIXMLDocumentはMSXMLを使うため、コンソールやワーカースレッドではLoadXMLDataの前にCoInitializeを呼び出します

コスト比較とライブラリの選択

このアプローチの主な利点は、パフォーマンスとメモリの安全性です。完全なワークブックDOM(ドキュメントオブジェクトモデル)のインスタンス化を避け、docProps/core.xmlまたはOLEプロパティストリームのみを解析することで、アプリケーションのフットプリントは信じられないほど小さく保たれます。ネットワーク共有全体で10,000個のExcelファイルのインデックスを作成している場合、それぞれを完全に解析しようとするとメモリがスラッシングし、何時間もかかります。専用のプロパティ抽出では、同じタスクが数秒で完了します

さらに、ストリームをネイティブに読み取ることで、Excel.exeを呼び出すことなく、アプリケーションをバックグラウンドサービスとして、またはヘッドレスLinuxサーバー上で実行できるようになります。これは最新のスケーラブルなアーキテクチャにとって不可欠な要件です

注: 包括的なExcel解析およびメタデータ抽出ツールは、HotXLS VCL Componentで利用できます