مقاله فنی

استخراج اطلاعات خلاصه سند از فایل‌های Excel در دلفی

از یک خط لوله بخواهید ده هزار صفحه‌گسترده را بر اساس نویسنده، شرکت یا تاریخ آخرین تغییر مسیریابی کند، و بدترین کاری که می‌تواند بکند این است که هر کتاب کار را به‌طور کامل باز کند. پاسخ‌ها در ویژگی‌های سند فایل نشسته‌اند، چیزی که دنیای Office به آن Document Summary Information می‌گوید: لایه فراداده‌ای که Windows Search نمایه می‌کند، SharePoint بر اساس آن بایگانی می‌کند، و Excel در پنجره Properties خود نشان می‌دهد. آن لایه حداکثر چند کیلوبایت است و در هر دو قالب Excel در جایی خوش‌مستند زندگی می‌کند. ترفند این است که از دلفی به آن برسید بدون آن‌که هزینه میلیون‌ها سلولی را بپردازید که به آن‌ها نیازی ندارید

سه مسیر واقعی وجود دارد، و تفاوتشان کمتر در چیزی است که برمی‌گردانند و بیشتر در چیزی است که از ماشین اجراکننده مطالبه می‌کنند. اتوماسیون COM خود Excel را می‌راند و همه چیز را می‌خواند، به قیمت دسکتاپ. قالب .xls ویژگی‌هایش را در جریان‌های OLE property set نگه می‌دارد که ویندوز برایتان تجزیه می‌کند. قالب .xlsx آن‌ها را در دو بخش کوچک XML درون یک zip نگه می‌دارد که RTL دلفی خودش می‌تواند باز کند. کد کارآمد برای هر کدام در ادامه می‌آید، با هزینه‌هایی که صریح بیان شده‌اند

نمودار سه مسیر دلفی به Document Summary Information فایل‌های Excel: اتوماسیون COM که خود Excel را می‌راند، جریان‌های OLE property set برای فایل‌های xls، و تجزیه XML بخش docProps در OOXML برای بسته‌های xlsx
اتوماسیون COM پوشش کامل را به قیمت یک Excel دسکتاپ دارای مجوز و چند ثانیه برای هر فایل می‌خرد، در حالی که دو مسیر بومی قالب فقط ظرف‌های فراداده را در چند میلی‌ثانیه می‌خوانند. آنچه هر مسیر برمی‌گرداند تقریباً یکسان است — آنچه از ماشین میزبان مطالبه می‌کند یکسان نیست

مسیر ۱: اتوماسیون COM همه چیز را می‌خواند، به قیمت دسکتاپ

اتوماسیون تنها مسیری است که از طریق یک مدل شیء واحد پوشش کامل دارد: مجموعه خلاصه استاندارد، مجموعه گسترده با Company و Manager، و ویژگی‌های سفارشی تعریف‌شده توسط کاربر، همگی از طریق BuiltinDocumentProperties و CustomDocumentProperties در دسترس‌اند. همه چیز به‌صورت OleVariant می‌رسد، و این API یک عادت دارد که بهتر است پیش از گزیده‌شدن بدانید: ویژگی داخلی‌ای که هرگز مقداردهی نشده خالی برنمی‌گردد، بلکه به‌محض دست‌زدن به Value یک EOleException پرتاب می‌کند. تابع کمکی زیر آن را به‌جای خطا، «تنظیم‌نشده» تلقی می‌کند

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 := '';   // ویژگی وجود دارد اما هرگز مقداردهی نشده است
    end;
  end;

begin
  Excel := CreateOleObject('Excel.Application');
  try
    Excel.DisplayAlerts := False;
    Book := Excel.Workbooks.Open(FileName, 0, True);   // فقط‌خواندنی
    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;   // در هر مسیری به این‌جا برسید، وگرنه EXCEL.EXE باقی می‌ماند
    Excel := Unassigned;
  end;
end;

حالا صورت‌حساب. Excel باید روی هر ماشینی که این کد روی آن اجرا می‌شود نصب باشد، که به‌تنهایی بیشتر سرورها را کنار می‌گذارد، و سیاست پشتیبانی مایکروسافت صریح است که Office نه برای اتوماسیون بدون مراقب سمت سرور طراحی شده و نه برای آن مجوز دارد. CreateOleObject یک EXCEL.EXE کامل راه می‌اندازد و Workbooks.Open کل کتاب کار را تجزیه می‌کند، پس انتظار داشته باشید پیش از بازگشت نخستین ویژگی، تقریباً دو تا چهار ثانیه برای هر فایل صرف شود. و try..finally دور Quit تزئینی نیست: استثنایی که بین CreateOleObject و Quit بگریزد یک EXCEL.EXE یتیم به جا می‌گذارد که قفل فایل را نگه داشته و تا وقتی اجرای بعدی روی آن شکست نخورد نامرئی است. استفاده مجدد از یک نمونه Excel در طول یک دسته هزینه راه‌اندازی را سرشکن می‌کند اما ریسک را متمرکز می‌کند، چون یک پنجره سرگردان روی دسکتاپ پنهان هر فایلی را که پشت آن صف کشیده متوقف می‌کند

مسیر ۲: .xls ویژگی‌ها را در جریان‌های OLE property set ذخیره می‌کند

یک کتاب کار BIFF8 یک فایل مرکب OLE است، یک سیستم فایل مینیاتوری از storage‌ها و stream‌ها. داده سلول‌ها در جریان Workbook زندگی می‌کند؛ فراداده کنار آن در دو جریان property set قرار دارد که نامشان با نویسه کنترلی #5 شروع می‌شود: \005SummaryInformation برای فیلدهای کلاسیک و \005DocumentSummaryInformation برای فیلدهای گسترده و سفارشی. درون هر کدام یک property set دودویی با چیدمان MS-OLEPS نشسته است، با بخش‌هایی که با یک شناسه قالب (FMTID) کلیدگذاری شده‌اند و ویژگی‌هایی که با یک شناسه ویژگی عدد صحیح کلیدگذاری شده‌اند. بخش خلاصه FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9} است، که در آن PIDSI_TITLE برابر $02 و PIDSI_AUTHOR برابر $04 است؛ Company ($0F) و Manager ($0E) در بخش خلاصه سند زندگی می‌کنند، و ویژگی‌های سفارشی در بخش دومی پشت یک فرهنگ نام

کالبدشناسی دلفی از یک فایل مرکب xls با قالب BIFF8 که جریان Workbook را کنار property set‌های SummaryInformation و DocumentSummaryInformation قرار می‌دهد، با زنجیره دسترسی StgOpenStorageEx به IPropertySetStorage
یک فایل xls داده سلول‌ها و ویژگی‌های سند را به‌صورت جریان‌های هم‌سطح در یک فایل مرکب OLE ذخیره می‌کند. ویندوز property set‌های دودویی را برایتان تجزیه می‌کند، پس کد دلفی نه به چیدمان‌های MS-OLEPS دست می‌زند و نه به code page‌ها

خبر خوب این است که روی ویندوز هرگز آن بایت‌ها را خودتان تجزیه نمی‌کنید. Structured storage جریان‌ها را از طریق IPropertySetStorage در معرض می‌گذارد، و کد زیر همان‌طور که نشان داده شده در برابر واحدهای استاندارد RTL کامپایل می‌شود

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: وجود ندارد
  try
    case Value.vt of
      VT_LPSTR:  Result := string(AnsiString(Value.pszVal));
      VT_LPWSTR: Result := Value.pwszVal;
    end;
  finally
    PropVariantClear(Value);
  end;
end;

// نحوه استفاده: Writeln('Author: ', ReadXlsSummaryString('ledger.xls', PIDSI_AUTHOR));

یک حرف صادقانه درباره آنچه این قطعه پنهان می‌کند. رشته‌ها می‌توانند به‌صورت VT_LPWSTR یا VT_LPSTR برسند، و در حالت ANSI بایت‌ها با code page خود property set رمزگذاری شده‌اند، که خودش به‌صورت ویژگی ۱ همان بخش ذخیره می‌شود؛ پس تبدیل بالا فقط وقتی دقیق است که آن code page با code page سیستم مطابقت داشته باشد. زمان‌مهرها به‌صورت VT_FILETIME در UTC برمی‌گردند. ویژگی‌های سفارشی یعنی بازکردن بخش تعریف‌شده توسط کاربر، FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}، و پیمایش فرهنگ نام آن. IPropertyStorage همه این‌ها را روی ویندوز جذب می‌کند؛ نوشتن تجزیه‌گر MS-OLEPS خودتان برای محیطی بدون structured storage یک پروژه واقعی است، نه یک بعدازظهر

مسیر ۳: .xlsx بخش docProps را به‌صورت XML درون zip نگه می‌دارد

این مسیری است که بیشتر خط لوله‌ها واقعاً به آن نیاز دارند، چون فایل‌های جدید نزدیک به دو دهه است که .xlsx هستند. یک کتاب کار OOXML یک بسته zip است، و ویژگی‌هایش بر اساس هدف در بخش‌های کوچکی تقسیم شده‌اند: docProps/core.xml فیلدهای Dublin Core را نگه می‌دارد، dc:title، dc:creator، cp:lastModifiedBy، به‌علاوه dcterms:created و dcterms:modified به‌صورت زمان‌مهرهای W3CDTF در UTC، در حالی که docProps/app.xml فیلدهای سطح برنامه مانند Company و AppVersion را نگه می‌دارد، و docProps/custom.xml ویژگی‌های سفارشی را. چون فهرست مرکزی zip هر بخش را مستقیماً پیدا می‌کند، خواندن آن‌ها بدون توجه به اندازه کتاب کار فقط چند کیلوبایت هزینه دارد. TZipFile و IXMLDocument، هر دو در RTL عرضه‌شده، تمام کار را انجام می‌دهند

دلفی: چیدمان یک بسته zip از نوع xlsx که اعضای XML بخش docProps یعنی core، app و custom را کنار بخش‌های کاربرگ نشان می‌دهد، با قواعد تولیدی برای کاوش بخش‌های اختیاری و تطبیق فضای نام‌ها
داده کاربرگ بر یک بسته xlsx غالب است، با این حال فراداده در سه عضو کوچک اختیاری کنار آن نشسته است. دسترسی تصادفی از طریق فهرست مرکزی 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;

دو جزئیات این را در تولید مقاوم نگه می‌دارد. نخست، بخش‌ها اختیاری‌اند: یک بسته حداقلی بدون هیچ docProps بر اساس ECMA-376 کاملاً معتبر است، و به همین دلیل کد به‌جای فرض‌کردن، با IndexOf کاوش می‌کند. دوم، عناصر را با نام محلی و URI فضای نام تطبیق دهید، همان‌طور که FindNode در بالا انجام می‌دهد، هرگز با پیشوند لفظی؛ dc: و cp: قراردادهای نویسنده Excel هستند، و فایل‌های تولیدشده توسط مولدهای دیگر آزادند پیشوندهای متفاوتی انتخاب کنند. یک نکته محیطی: فروشنده پیش‌فرض IXMLDocument همان MSXML است، پس یک برنامه کنسولی یا نخ کارگر باید پیش از LoadXMLData تابع CoInitialize را فراخوانی کند، وگرنه نخستین تجزیه با یک خطای COM می‌میرد

برگه هزینه، و زمانی که یک کتابخانه بر هر دو تجزیه‌گر برتری دارد

اندازه‌گیری‌شده روی یک ماشین توسعه معمولی، مسیر COM وقتی نشست اتوماسیون برای هر فایل ساخته می‌شود تقریباً به دو تا چهار ثانیه برای هر فایل می‌رسد، که تقریباً همه‌اش راه‌اندازی EXCEL.EXE به‌علاوه تجزیه کامل کتاب کار است، و هر جا اجرا شود به یک Excel نصب‌شده و دارای مجوز نیاز دارد. دو مسیر مستقیم فقط ظرف‌های فراداده را می‌خوانند، در چند میلی‌ثانیه تک‌رقمی برای هر فایل تمام می‌شوند، و به هیچ چیزی فراتر از آنچه یک فایل اجرایی دلفی از قبل پیوند می‌دهد نیاز ندارند. در یک اشتراک ده هزار فایلی، این تفاوت بین بیشتر یک روز کاری و کمتر از یک دقیقه است، بدون هیچ پرسشی درباره استقرار Office

گیر مسیرهای مستقیم این است که دو تا هستند. خط لوله‌ای که هر دو قالب را می‌پذیرد دو تجزیه‌گر با دو مجموعه حالت خرابی ناهمپوشان نگه می‌دارد، code page‌ها و انواع PROPVARIANT در یک سو، فضای نام‌ها و بخش‌های اختیاری در سوی دیگر، و هیچ‌کدام قالب دیگری را نمی‌خواند. آن بار نگهداری دلیل استفاده از یک کتابخانه بومی است: HotXLS، کتابخانه صفحه‌گسترده Object Pascal شرکت losLab برای دلفی و C++Builder روی ویندوز، همان فیلدها را به‌صورت ویژگی‌های ساده کتاب کار در معرض می‌گذارد، Title، Author، Company، Created و بقیه، که Open برای .xls و .xlsx به یک اندازه پر می‌کند، بدون نصب Excel و بدون هیچ‌کدام از لوله‌کشی ظرف‌ها در بالا. ویژگی‌ها را به‌عنوان بخشی از بازکردن کامل کتاب کار می‌خواند نه یک کاوش فقط‌فراداده، پس با خط لوله‌هایی که به هر حال به داده سلول‌ها دست می‌زنند جور است؛ سطح کامل ویژگی‌ها روی هر دو نما، شامل سمت نوشتن، در مقاله ما درباره تنظیم ویژگی‌های سند Excel با HotXLS پوشش داده شده است

توجه: ابزارهای کامل تجزیه Excel و استخراج فراداده در کامپوننت VCL دلفی HotXLS در دسترس است