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