自動化されたパイプラインで大量の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.xml、docProps/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で利用できます