مقال تقني

HotXLS Delphi Component: large workbook performance في Delphi

تحدد المقالة مصادر الذاكرة في المصنف الكبير، وتوضح أن StreamingWrite يصلح مسار الحفظ لا مسار البناء. كما توصي بإعادة استخدام style pools، واستبدال حركة Variant لكل خلية بـ row callbacks، وفحص الملف قبل تحميله

عندما يتجاوز تصدير من 300,000 صف ميزانيته من الذاكرة، عادة ما يُلام عدد الصفوف. عدد الصفوف عادة بريء. الأجزاء المكلفة من مصنف كبير هي تلك التي تُنشأ كأثر جانبي: تجمّع أنماط ينمو بإدخال واحد لكل خلية لأن التنسيق أُضيف داخل الحلقة، وXML لورقة عمل يُجمَّع كسلسلة نصية واحدة عملاقة وقت الحفظ، ومليون جسم صيغة متطابق يُخزَّن واحدًا تلو الآخر. HotXLS، مكتبة Delphi الأصلية من losLab لملفات XLS وXLSX، تمنحك مقبضًا محددًا لكل واحدة من هذه التكاليف. لا شيء منها مُفعَّل افتراضيًا، لأن كل واحد يغيّر مقايضة، فمعرفة أي مقبض يطابق أي عرَض هي المهارة الفعلية للأداء

نظامان للذاكرة في مهمة مصنف كبير بـ HotXLS داخل Delphi: نموذج الخلايا في الذاكرة الذي تبنيه حلقة التوليد، إضافة إلى سلسلة XML المسلسلة لأكبر ورقة أثناء حفظ افتراضي، والذي يزيله StreamingWrite
يفشل حلقة البناء واستدعاء الحفظ في نظامَي ذاكرة مختلفين، فيسطّح StreamingWrite الذروة وقت الحفظ وحدها بينما تحتاج ذاكرة مسار البناء روافع بركة الأنماط ورد النداء

أين ينفق مصنف كبير الذاكرة

هناك نظاما ذاكرة متمايزان يجب التفكير بهما. أثناء التوليد، ينمو نموذج الخلايا في الذاكرة مع كل خلية تلمسها: القيم، والتنسيقات، والصيغ كلها تصبح كائنات أو إدخالات تجمّع. أثناء الحفظ، يعرض مسار XLSX الافتراضي بالإضافة إلى ذلك XML كل ورقة عمل في سلسلة نصية عريضة قبل ضغطها في حاوية الأرشيف المضغوط، فالذروة القصوى للاستخدام هي النموذج زائد الشكل المسلسل لأكبر ورقة. مهمة تنجو من حلقة البناء ثم تموت داخل SaveAs تصطدم بالنظام الثاني، لا الأول، وإصلاح أحدهما لا يفعل شيئًا للآخر

حجم الملف يتبع قاعدة ذات صلة: الخلايا مساهم واحد فقط، إلى جانب الأنماط، والسلاسل المشتركة، والصيغ، والصور، والتعليقات. تمريرة تدقيق بـForEachCell وأعداد المجموعات لكل ورقة تخبرك أي مورد يهيمن فعليًا على ملف مشكلة قبل أن تحسّن الجانب الخاطئ. دقة قياس واحدة: Sheet.Cells.Count في جهة XLSX تُبلغ عن عدد الخلايا المُجسَّدة في المخزن المتفرق، لا مساحة النطاق المستخدم. ورقة تشغل بياناتها مستطيلًا من 1000 في 50 مع نصف الخلايا فارغة تُحصى بنحو 25,000، لا 50,000. ذلك التمييز مهم عندما تقارن ملف عميل "ضخمًا" مع عينات اختبارك، لأن مساحة النطاق المستخدم وتعداد الخلايا الفعلي يمكن أن يختلفا برتبة حجم كاملة في تخطيطات مالية متفرقة

StreamingWrite يصلح مسار الحفظ، لا مسار البناء

ضبط TXLSXWorkbook.StreamingWrite := True يحوّل SaveAs إلى مسلسل بث يكتب XML ورقة العمل مباشرة إلى تدفق الأرشيف المضغوط، ويزيل الوسيط النصي لكل ورقة. القيمة الافتراضية False لتوافق سلوكي، وتفعيلها تغيير من سطر واحد:

Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Bulk');
  for R := 1 to 100000 do
  begin
    Sheet.Cells[R, 1].Value := R;
    Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
    Sheet.Cells[R, 3].Value := R * 1.5;
  end;
  Book.StreamingWrite := True;   // XML الورقة يُبث إلى حاوية الأرشيف المضغوط
  Book.SaveAs('bulk.xlsx');
finally
  Book.Free;
end;

كن دقيقًا بشأن ما يشتريه هذا: نموذج الخلايا الذي بنته الحلقة يشغل بالضبط نفس قدر الذاكرة كما كان من قبل. StreamingWrite يسطّح ذروة وقت الحفظ، وهذا هو الفرق بين مهمة دفعية تكتمل وأخرى تفشل عند علامة 95%. إذا استنفدت حلقة البناء نفسها الذاكرة، فالمقبضان اللذان تحتاجهما هما التاليان

تجمّعات الأنماط: أضف مرة، أعد استخدام الفهرس

تنسيق XLSX في HotXLS قائم على التجمّع: Book.Fonts.Add(...) وFills.AddSolid(...) وBorders.Add(...) تعيد فهرس تجمّع يبدأ من 0 تشير إليه الخلايا. استدعاء Fonts.Add بمعاملات متطابقة داخل حلقة يُزال تكراره، فهو يهدر الوقت لا المساحة. أما Alignments.Add فتتصرف بشكل مختلف: إنها تعيد كائنًا جديدًا لكل استدعاء، فإنشاء محاذاة لكل خلية ينمّي التجمّع خطيًا مع عدد الصفوف. عادة واحدة تغطي الحالتين. حلّ كل فهرس تجمّع مرة واحدة، خارج الحلقة، وأسند الفهارس داخلها

مقارنة استخدام مخزن الأنماط في HotXLS داخل Delphi: كائن Alignments.Add طازج يُنشأ مرة لكل صف ينمّي المخزن خطيًا، بينما يعاد استخدام فهرس Fonts.Add مرفوع يُحل مرة فوق الحلقة من كل خلية مع إزاحة الفهرس القائم على الصفر بمقدار واحد
حل كل فهرس خط وتعبئة وحد ولاء مرة واحدة خارج الحلقة، ثم أسند فهرس البركة ذا الأساس الصفري مُزاحاً واحداً داخلها
// ارفع عمليات بحث التجمّع خارج الحلقة الساخنة
HeaderFont := Book.Fonts.Add('Calibri', 11, True, False);   // فهرس تجمّع يبدأ من 0
for C := 1 to 24 do
  Sheet.Cells[1, C].FontIndex := HeaderFont + 1;            // الخلايا تخزّن فهرسًا يبدأ من 1؛ 0 = افتراضي

+ 1 ليست خطأ كتابيًا وإغفالها هو الخطأ الكلاسيكي المولّد للعرَض هنا: التجمّعات توزّع فهارس تبدأ من 0، بينما تعامل خصائص جهة الخلية القيمة 0 كـ"افتراضي"، فكل فهرس تجمّع يجب إزاحته بواحد عند الإسناد. أخطئ فيه بالإغفال وستُعرَض ترويساتك بهدوء بخط المصنف الافتراضي، عيب لا ينتبه إليه أحد حتى مراجعة العلامة التجارية

استبدل حركة Variant لكل خلية بدوال استدعاء خلفي للصفوف

كل Sheet.Cells[R, C].Value := X يتضمن بحثًا عن خلية أو إنشاءها بالإضافة إلى إسناد Variant. عند بضع مئات الآلاف من الخلايا، تصبح تلك النفقة العامة لكل وصول قابلة للقياس في ملفات التعريف. يوفر HotXLS واجهات استدعاء خلفي بالجملة على كلتا الواجهتين (ForEachCell وForEachRow للقراءة، وWriteCells وWriteRows للكتابة) تنقل التكرار إلى داخل المحرك وتسلّم كودك صفوفًا كاملة في كل مرة:

procedure TLedgerExport.FillRow(Sender: TObject;
  SheetIndex, Row, FirstCol, LastCol: Integer;
  var Values: Variant; var Skip: Boolean; var Cancel: Boolean);
begin
  if Row > FCount then
  begin
    Cancel := True;     // أوقف الكتابة بأكملها
    Exit;
  end;
  Values := VarArrayOf([FRows[Row - 1].Account,
                        FRows[Row - 1].PostedOn,
                        FRows[Row - 1].Amount]);
end;

// استدعاء محرك واحد بدلًا من مئات الآلاف من إصابات الخصائص
Sheet.WriteRows(1, 1, FCount, 3, FillRow);

علامة Skip الخاصة بدالة الاستدعاء الخلفي تترك صفًا دون لمس دون إجهاض العملية، وCancel تنهي العملية مبكرًا، وهذا مفيد عندما يكون المصدر قارئًا تكتشف طوله أثناء المضي قدمًا. اقرن WriteRows للبناء مع StreamingWrite للحفظ ولن يبقى لمسار التوليد أي نقطة ساخنة لكل خلية

مقابض جهة القراءة في واجهة XLS

ملفات ‎.xls‎ القديمة الكبيرة لها عدتها الخاصة. _DisableGraphics := True قبل Open تتخطى تحليل طبقة الرسم بأكملها، وهذا يسرّع تحميل مصنفات تحمل سنوات من الأشكال والصور المضمّنة المتراكمة. القيد صارم: طبقة الرسم تكون عندها غائبة عن النموذج، فحفظ مصنف كهذا يكتب ملفًا دون رسومه. احتفظ بهذه العلامة لمهام التحليل للقراءة فقط. SetTempDir تعيد توجيه الملفات المؤقتة لكاتب BIFF، وهذا مهم على خوادم يكون فيها موقع المؤقت الافتراضي محدودًا بحصة أو موجودًا على تخزين بطيء. UseSharedFormulas تجمّع أجسام الصيغ المتكررة في سجلات صيغ مشتركة، وتقلّص الملفات حيث يتكرر عمود صيغة عبر ستين ألف صف

حلقات القراءة عبر بيانات XLS لها فخ فهرسة يستحق الإشارة إليه لأنه يضاعف العمل عند معالجته دفاعيًا ويُفسد النتائج عند إغفاله: UsedRange تُبلغ عن حدود FirstRow وLastRow وFirstCol وLastCol بدءًا من 0، بينما Cells.Item[Row, Col] يبدأ من 1. مسح يجتاز النطاق المستخدم يجب أن يضيف واحدًا إلى كل إحداثي عند الوصول إلى الخلية، كما في Cells.Item[Row + 1, Col + 1]، وإلا سيقرأ شبكة منزاحة قطريًا بخلية واحدة، مسقطًا بصمت آخر صف وعمود ومضمّنًا صفًا أول وهميًا. دالة الاستدعاء الخلفي ForEachCell تتجنب عدم التطابق هذا كليًا، وهذا سبب إضافي لتفضيلها في عمليات المسح لورقة كاملة

افحص الملفات قبل تحميلها

أرخص عملية على مصنف كبير هي التي تتجنبها. GetSheetNames على كلتا الواجهتين تسرد أوراق عمل الملف دون تحميل بيانات الخلايا. تنفيذ XLSX يقرأ فقط بيان المصنف داخل الأرشيف المضغوط ويترك كائن المصنف صراحة غير مملوء، وتتوقف واجهة XLS عن المسح عند أول حد تدفق فرعي. هذا يجعلها فحص ما قبل الرحلة الصحيح لسؤال "أي ورقة يجب أن تستهدفها مهمة الاستيراد هذه"، وCanReadEncrypted تجيب عن "هل هذه حاوية مشفّرة" قبل محاولة Open محكوم عليها بالفشل

تدفق فحص مسبق لملف Excel غير معروف في Delphi بـ HotXLS: يسرد GetSheetNames أوراق العمل دون تحميل بيانات الخلايا، ويعني كود إرجاع عند الصفر أو دونه إفراغ القائمة وفشلًا، ويوسم CanReadEncrypted الحاويات المشفرة قبل Open محكوم عليه بالفشل، وعندها فقط يعمل التحميل الكامل
يجيب GetSheetNames وCanReadEncrypted عن أي ورقة تُستهدف وهل الوعاء مقروء قبل تحليل أي بيانات خلايا
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
  if Book.GetSheetNames('big-unknown.xlsx', Names) <= 0 then
    raise Exception.Create('cannot enumerate sheets');   // الفشل يفرغ القائمة
  // اختر الورقة الهدف، ثم قرر ما إذا كان Open كامل يستحق العناء
finally
  Book.Free;
  Names.Free;
end;

لاحظ اصطلاح رمز الإرجاع: دوال الفحص هذه تشير إلى الفشل بقيم عند الصفر أو أقل وتفرغ قائمة الخرج، لذا اختبر <= 0 بدلًا من المقارنة بقيمة نجاح محددة واحدة

تحجيم النهج بما يناسب المهمة

لخطوط الأنابيب غير المراقَبة التي تولّد ملفات كبيرة كثيرة بالتتابع، عادتان إضافيتان تكملان الصورة. كائنات المصنف ليست آمنة لتعدد الخيوط في المشاركة، لكن لا شيء يمنع مصنفًا مستقلًا واحدًا لكل خيط عامل، وهذا يوازي تحويل الدفعات بنظافة. وعندما يذهب الخرج إلى HTTP بدلًا من القرص، تتحد حمولات حفظ TStream الزائدة مع StreamingWrite حتى لا تتجسد استجابة كبيرة أبدًا كملف مؤقت. هامش تشغيلي واحد ينطبق: حفظ التدفق يكتب من الموضع الحالي دون إرجاع المؤشر، فاضبط Position := 0 قبل تسليم التدفق إلى إطار عمل الاستجابة. مقالة الكتابة المتدفقة والمهام الدفعية تطوّر ذلك النمط في جهة الخادم، ومقالة تصدير قاعدة البيانات تُظهر أين تندمج هذه المقابض في تقرير مدفوع بمجموعة بيانات

أخيرًا، احتفظ بعيّنة اختبار واحدة لأسوأ حالة لكل عائلة تقارير وقِس زمنها في التكامل المستمر. انحدارات الأداء في توليد المستندات نادرًا ما تعلن عن نفسها. نمط أُضيف داخل حلقة أو فحص استُبدل بـOpen كامل لا يغيّر شيئًا وظيفيًا، والدفعة الليلية تستغرق ببساطة أربعين دقيقة إضافية. اختبار مُوقَّت على عيّنة تمثيلية من نصف مليون خلية يحوّل ذلك الانحراف إلى بناء فاشل بدلًا من حادثة تشغيلية

نسخ التقييم، والمشاريع التجريبية مع مثال توليد بالجملة، والمرجع الكامل لواجهة API متوفرة على صفحة HotXLS Delphi Component