اطلب من مسار عمل (pipeline) توجيه عشرة آلاف جدول بيانات حسب المؤلف، أو الشركة، أو تاريخ آخر تعديل، وأسوأ شيء يمكنه القيام به هو فتح كل مصنف (workbook) بالكامل. الإجابات موجودة في خصائص مستند الملف، وهو ما يطلق عليه عالم Office اسم معلومات ملخص المستند (Document Summary Information): وهي طبقة البيانات الوصفية التي يفهرسها Windows Search، ويفرز SharePoint الملفات بها، ويعرضها Excel في مربع حوار الخصائص الخاص به. هذه الطبقة هي كيلوبايتات على الأكثر، وتعيش في مكان موثق جيدًا في كلا تنسيقي Excel. الحيلة تكمن في الوصول إليها من Delphi دون دفع ثمن المليون خلية التي لا تحتاجها
هناك ثلاث طرق حقيقية، وهي لا تختلف كثيرًا فيما تعيده بقدر ما تختلف فيما تتطلبه من الجهاز الذي يشغلها. تعمل أتمتة COM (COM automation) على تشغيل Excel نفسه وتقرأ كل شيء، بأسعار أجهزة سطح المكتب. يحتفظ تنسيق .xls بخصائصه في تدفقات مجموعة خصائص OLE التي سيقوم Windows بتحليلها لك. يحتفظ تنسيق .xlsx بها في جزأين صغيرين من XML داخل ملف zip يمكن لمكتبة تشغيل Delphi (RTL) فتحهما بمفردها. الكود العامل لكل منها يتبع ذلك، مع ذكر التكاليف بوضوح
الطريقة الأولى: تقرأ أتمتة COM كل شيء، بأسعار أجهزة سطح المكتب
الأتمتة هي الطريقة الوحيدة التي توفر تغطية كاملة من خلال نموذج كائن (object model) واحد: مجموعة الملخصات القياسية، والمجموعة الموسعة مع الشركة والمدير، والخصائص المخصصة المحددة من قبل المستخدم، ويمكن الوصول إليها جميعًا من خلال BuiltinDocumentProperties و CustomDocumentProperties. يصل كل شيء كـ OleVariant، ولدواجهة برمجة التطبيقات (API) عادة واحدة تستحق المعرفة قبل أن تتسبب في مشكلة: الخاصية المضمنة التي لم يتم تعيينها أبدًا لا تعود فارغة، بل تثير استثناء EOleException في اللحظة التي تلمس فيها Value. يعامل المساعد (helper) أدناه ذلك على أنه "لم يتم تعيينه" بدلاً من اعتباره فشلاً
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;
الآن التكلفة. يجب تثبيت Excel على كل جهاز يعمل عليه هذا الكود، وهو ما يستبعد معظم الخوادم بحد ذاته، وسياسة دعم Microsoft صريحة بأن Office غير مصمم ولا مرخص لأتمتة جانب الخادم (server-side automation) غير المراقبة. يقوم CreateOleObject بتشغيل EXCEL.EXE كامل و Workbooks.Open يحلل المصنف بأكمله، لذلك توقع ما يقرب من ثانيتين إلى أربع ثوانٍ لكل ملف قبل أن تعود الخاصية الأولى. و try..finally حول Quit ليست للزينة: الاستثناء الذي يفلت بين CreateOleObject و Quit يترك EXCEL.EXE يتيمًا يمسك بقفل على الملف، ويكون غير مرئي حتى يفشل التشغيل التالي بسببه. تؤدي إعادة استخدام مثيل Excel واحد عبر دفعة إلى إطفاء تكلفة بدء التشغيل ولكنها تركز الخطر، لأن مربع حوار واحد شارد على سطح المكتب المخفي يوقف كل ملف مصطف خلفه
الطريقة الثانية: تخزن ملفات .xls الخصائص في تدفقات مجموعة خصائص OLE
مصنف BIFF8 هو ملف مركب OLE، ونظام ملفات مصغر من التخزينات والتدفقات. تعيش بيانات الخلية في تدفق Workbook؛ وتعيش البيانات الوصفية بجانبها في تدفقين لمجموعة الخصائص تبدأ أسماؤهما بحرف التحكم رقم 5: \005SummaryInformation للحقول الكلاسيكية و \005DocumentSummaryInformation للحقول الموسعة والمخصصة. يقع داخل كل منها مجموعة خصائص ثنائية بتنسيق MS-OLEPS، مع أقسام محددة بمعرف تنسيق (FMTID) وخصائص محددة بمعرف خاصية عدد صحيح. قسم الملخص هو FMTID {F29F85E0-4FF9-1068-AB91-08002B27B3D9}، حيث PIDSI_TITLE هو $02 و PIDSI_AUTHOR هو $04؛ تعيش الشركة ($0F) والمدير ($0E) في قسم ملخص المستند، وتوجد الخصائص المخصصة في قسم ثانٍ خلف قاموس أسماء
الأخبار السارة هي أنك على Windows لن تقوم أبدًا بتحليل هذه البايتات بنفسك. يعرض التخزين المهيكل (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: 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));
كلمة صادقة حول ما تخفيه المقتطفات (snippet). يمكن أن تصل السلاسل كـ VT_LPWSTR أو كـ VT_LPSTR، وفي حالة ANSI يتم تشفير البايتات في صفحة الكود (code page) الخاصة بمجموعة الخصائص نفسها، والتي يتم تخزينها بحد ذاتها كخاصية 1 من القسم، لذلك فإن التحويل (cast) أعلاه يكون دقيقًا فقط عندما تتطابق صفحة الكود تلك مع صفحة النظام. تعود الطوابع الزمنية كـ VT_FILETIME في التوقيت العالمي المنسق (UTC). تعني الخصائص المخصصة فتح القسم المحدد من قبل المستخدم، FMTID {D5CDD505-2E9C-101B-9397-08002B2CF9AE}، والسير في قاموس أسمائه. يمتص IPropertyStorage كل ذلك على Windows؛ كتابة محلل MS-OLEPS الخاص بك لبيئة بدون تخزين مهيكل هو مشروع حقيقي، وليس مهمة تستغرق فترة ما بعد الظهيرة
الطريقة الثالثة: تحتفظ ملفات .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 بالحقول على مستوى التطبيق مثل الشركة و 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 لمساحة الاسم (namespace URI)، كما يفعل FindNode أعلاه، وليس أبدًا بواسطة البادئة الحرفية؛ dc: و cp: هي اصطلاحات لكاتب Excel، والملفات التي تنتجها المولدات الأخرى حرة في اختيار بادئات مختلفة. ملاحظة بيئية واحدة: البائع الافتراضي لـ IXMLDocument هو MSXML، لذلك يجب أن يستدعي تطبيق وحدة التحكم (console application) أو سلسلة مسارات العمل (worker thread) CoInitialize قبل LoadXMLData، وإلا فإن التحليل الأول سيموت بخطأ COM
ورقة التكلفة، ومتى تتفوق المكتبة على كلا المحللين
إذا تم القياس على جهاز مطور عادي، فإن طريقة COM تستغرق حوالي ثانيتين إلى أربع ثوانٍ لكل ملف عندما يتم إنشاء جلسة الأتمتة لكل ملف، وكل ذلك تقريبًا بدء تشغيل EXCEL.EXE بالإضافة إلى تحليل كامل للمصنف، ويتطلب تثبيت Excel مرخص أينما كان يعمل. تقرأ الطريقتان المباشرتان حاويات البيانات الوصفية فقط، وتنتهيان في أجزاء من الألف من الثانية من خانة واحدة لكل ملف، ولا تحتاجان إلى تثبيت أي شيء يتجاوز ما يربطه بالفعل ملف Delphi القابل للتنفيذ. عبر مشاركة من عشرة آلاف ملف، هذا هو الفرق بين معظم يوم عمل وأقل من دقيقة، دون إرفاق أي سؤال حول نشر Office
المشكلة في الطرق المباشرة هي أن هناك طريقتين. مسار العمل الذي يقبل كلا التنسيقين يحافظ على محلين اثنين مع وضعي فشل منفصلين، صفحات الكود وأنواع PROPVARIANT من جانب، ومساحات الأسماء والأجزاء الاختيارية من جانب آخر، ولا يقرأ أي منهما تنسيق الآخر. عبء الصيانة هذا هو مبرر وجود مكتبة أصلية: يعرض HotXLS، وهي مكتبة جداول بيانات Object Pascal من losLab لـ Delphi و C++Builder على Windows، نفس الحقول كخصائص مصنف عادية، العنوان (Title)، والمؤلف (Author)، والشركة (Company)، وتاريخ الإنشاء (Created)، والبقية، مأهولة بواسطة Open لملفات .xls و .xlsx على حد سواء، بدون تثبيت Excel وبدون أي من سباكة الحاويات (container plumbing) المذكورة أعلاه. تقرأ الخصائص كجزء من فتح مصنف كامل بدلاً من فحص البيانات الوصفية فقط، لذا فهي تناسب مسارات العمل التي تستمر في لمس بيانات الخلية على أي حال؛ السطح الكامل للخصائص على كلتا الواجهتين، بما في ذلك جانب الكتابة، تمت تغطيته في مقالنا حول تعيين خصائص مستندات Excel باستخدام HotXLS
ملاحظة: تتوفر أدوات كاملة لتحليل Excel واستخراج البيانات الوصفية في مكون HotXLS VCL