از یک خط لوله بخواهید ده هزار صفحهگسترده را بر اساس نویسنده، شرکت یا تاریخ آخرین تغییر مسیریابی کند، و بدترین کاری که میتواند بکند این است که هر کتاب کار را بهطور کامل باز کند. پاسخها در ویژگیهای سند فایل نشستهاند، چیزی که دنیای Office به آن Document Summary Information میگوید: لایه فرادادهای که Windows Search نمایه میکند، SharePoint بر اساس آن بایگانی میکند، و Excel در پنجره Properties خود نشان میدهد. آن لایه حداکثر چند کیلوبایت است و در هر دو قالب Excel در جایی خوشمستند زندگی میکند. ترفند این است که از دلفی به آن برسید بدون آنکه هزینه میلیونها سلولی را بپردازید که به آنها نیازی ندارید
سه مسیر واقعی وجود دارد، و تفاوتشان کمتر در چیزی است که برمیگردانند و بیشتر در چیزی است که از ماشین اجراکننده مطالبه میکنند. اتوماسیون COM خود Excel را میراند و همه چیز را میخواند، به قیمت دسکتاپ. قالب .xls ویژگیهایش را در جریانهای OLE property set نگه میدارد که ویندوز برایتان تجزیه میکند. قالب .xlsx آنها را در دو بخش کوچک XML درون یک zip نگه میدارد که RTL دلفی خودش میتواند باز کند. کد کارآمد برای هر کدام در ادامه میآید، با هزینههایی که صریح بیان شدهاند
مسیر ۱: اتوماسیون 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) در بخش خلاصه سند زندگی میکنند، و ویژگیهای سفارشی در بخش دومی پشت یک فرهنگ نام
خبر خوب این است که روی ویندوز هرگز آن بایتها را خودتان تجزیه نمیکنید. 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 عرضهشده، تمام کار را انجام میدهند
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 در دسترس است