لنفترض أن خدمة Delphi ليلية تولّد ملف XLSX واحدا لكل عميل، بضع مئات من الملفات، بعضها بعرض 400,000 صف حلّل الأداء وستجد أن المفاجأة نادرا ما تكون حلقة ملء الخلايا إنها استدعاء SaveAs مع الكاتب الافتراضي، تُسلسَل كل ورقة عمل في سلسلة XML واحدة في الذاكرة قبل أن تُضغط تلك السلسلة في أرشيف OOXML المضغوط، وبالنسبة إلى ورقة عريضة يمكن للسلسلة المؤقتة أن تفوق حجم نموذج الخلايا الذي بُنيت منه فمهمة تبني بياناتها بارتياح وتستقر عند 800 ميغابايت تقفز فوق حد حاوية 2 غيغابايت أثناء الحفظ، ويكتب قاتل نفاد الذاكرة تقرير الخطأ عند الساعة 03:00 حين لا يراقب أحد HotXLS، مكتبة جداول البيانات الأصلية من losLab لـDelphi وC++Builder، لديها خاصية تستهدف تلك القفزة مباشرة: StreamingWrite وحولها رافعتان أخريان تقرران ما إذا كان عامل الدفعة سيبقى ضمن ميزانيته من الذاكرة والوقت، وهما ردود نداء الكتابة على مستوى الصف، وطريقة تصرف تجمّع الأنماط داخل حلقة ضيقة
ما يخزّنه مسار الحفظ الافتراضي مؤقتا، وما تغيّره StreamingWrite
يفضّل كاتب XLSX الافتراضي البساطة يعرض XML ورقة العمل كاملا، ثم يسلّم السلسلة النهائية إلى ضاغط الأرشيف هذا هو المقايضة الصحيحة للغالبية الساحقة من المصنفات، حيث يتسع XML الورقة كاملة في بضعة ميغابايتات يتوقف عن كونه صحيحا حين يصل الشكل المُسلسَل لورقة واحدة إلى مئات الميغابايتات XML جداول البيانات مطوّل: كل خلية رقمية تكلّف عشرات الأحرف من الترميز، والسلسلة التي تحمل كل ذلك يجب أن تكون متجاورة في الذاكرة على رسم بياني للذاكرة يصعب تفويت البصمة هضبة طويلة مسطحة أثناء ملء الصفوف، ثم قفزة مثلثية حادة أثناء SaveAs، ثم الانهيار بمجرد تفريغ الأرشيف المضغوط
ضبط Book.StreamingWrite := True يحوّل SaveAs إلى كاتب ورقة عمل يبث XML الورقة مباشرة إلى دفق الأرشيف المضغوط أثناء توليده لا تُخصَّص السلسلة الوسيطة أبدا، وتتسطح القفزة المثلثية داخل الضجيج
كن دقيقا بشأن ما تكسبه هذه الخاصية فعليا، لأن المبالغة في تقديرها تقود إلى خطط سعة خاطئة العلم يغيّر مسار الحفظ فقط بناء المصنف لا يزال يخصص نموذج الخلايا الكامل في الذاكرة، لذا الهضبة أثناء طور الملء بنفس ارتفاعها كما كانت ما يختفي هو قفزة التسلسل التي كانت تتراكم فوق تلك الهضبة عند الحفظ، وبالنسبة إلى مهمة تملأ 400 ألف صف تكون تلك القفزة عادة الفارق الكامل بين البقاء ضمن ميزانية الذاكرة وتجاوزها الخاصية افتراضيا False للحفاظ على السلوك التاريخي، لذا الاختيار الصريح لها سطر واحد تكتبه عمدا
تصدير جماعي مع تفعيل العلم
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // فهرس التجمع، يبدأ من صفر
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;
if (R mod 1000) = 0 then
Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // يبدأ من واحد عند الخلية
end;
Book.StreamingWrite := True; // بث XML الورقة مباشرة إلى الأرشيف المضغوط
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Cells[R, C] ينشئ الخلايا عند الطلب، ما يبقي جسم الحلقة نظيفا حدّان للشبكة يستحقان الحفظ في الذاكرة: 1,048,576 صفا و16,384 عمودا، معروضان كـXlsxMaxRow وXlsxMaxCol تغذية بيانات تتجاوز حد الصفوف يجب تقسيمها عبر أوراق في الكود الخاص بك لا شيء لاحقا يلاحظ التجاوز أو يصلحه لك، وينتهي الملف ببساطة مقطوعا عند الحد
ملء الصفوف دون عبء Variant لكل خلية
كل تعيين Cells[R, C].Value يدفع ثمن بحث عن خلية وتحويل Variant عند عشرة آلاف صف لا يلاحظ أحد ذلك عند مليون صف بعشرين عمودا لكل منها، يصبح عبء كل استدعاء التكلفة المهيمنة في طور الملء، ويشير المحلل إليه مباشرة واجهات الدفعات تتيح لك تسليم الكاتب صفا كاملا في كل مرة بدل ذلك WriteRows يدير رد نداء يوفر صفا واحدا في كل استدعاء
procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
LastCol: Integer; var Values: Variant; var Skip: Boolean;
var Cancel: Boolean);
begin
if not FReader.Next then
begin
Cancel := True; // مصدر البيانات نفد: أوقف بنظافة
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// املأ الصفوف 2..100001، الأعمدة A..C، بالسحب من القارئ
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
علم Cancel هو ما يحوّل نطاق صفوف ثابتا إلى "حتى N صف"، وهو الشكل الطبيعي حين يأتي عدد الصفوف من استعلام لم تنته من تنفيذه بعد Skip ألطف: يترك صفا فرديا فارغا دون إيقاف التشغيل إلى جانب ملء الخلايا، يتضح أن رد النداء موطن جيد للاهتمامات التشغيلية التي كانت لتُلصَق بحلقة الملء بطرق غير أنيقة عدّاد تقدم يتحرك كل ألف صف، ورمز إلغاء يُستطلَع من مجدول المهام، ومحدد معدل على القراءات من قاعدة بيانات المصدر: كل ذلك يعيش في مكان واحد بدل أن يُنسج عبر كود كتابة الخلايا في جانب القراءة، ForEachRow وForEachCell يعكسان النمط نفسه، وهو أمر مهم حين تستهلك مهمة دفعة ملفات كبيرة وتنتجها في آن واحد
تجمّعات الأنماط تكافئ الرفع خارج الحلقة
نموذج تنسيق XLSX هو مجموعة من التجمعات المشتركة Fonts.Add وFills.AddSolid وBorders.Add جميعها تعيد فهرس تجمّع يبدأ من صفر، وتشير الخلية إلى خط بتخزين ذلك الفهرس زائد واحد في FontIndex، حيث الصفر محجوز للافتراضي على مستوى المصنف الزائد واحد موجود بالضبط في المثال الجماعي أعلاه انسه وستلتقط الخلية بصمت النمط الخطأ، لأن إزاحة بواحد في فهرس تجمّع أنماط تبقى فهرسا صالحا ولا شيء يُطلق خطأ
الانضباط الذي يترتب على ذلك هو إنشاء كل كائن نمط قبل حلقة الصفوف والإشارة إلى فهرسه داخل الحلقة Fonts.Add يزيل التكرار من التعريفات المتطابقة، لذا استدعاؤه مرة لكل صف يهدر المعالج فقط Alignments.Add هو الفخ، لأنه يعيد إدخالا جديدا في كل استدعاء داخل حلقة من 100 ألف صف، هذا يدفن styles.xml تحت مئة ألف سجل محاذاة مكرر، ما يضخّم الملف على القرص ويبطئ كل فتح لاحق في Excel أثناء إعادة تحليل التكرارات ابنِ كل نمط مرة واحدة خارج الحلقة، ثم أشر إلى فهرسه بقدر ما تحتاج
الدفقات، والمجلدات المؤقتة، وحلقة الدفعة المحيطة بكل ذلك
لا شيء من هذا يتطلب نظام ملفات كلتا الواجهتين تحملان تحميلات TStream عبر سطح الإدخال والإخراج لديهما، Open وSaveAs وSaveAsCSV وSaveAsHTML وSaveAsODS من بينها، بحيث يستطيع عامل دفعة أن يرسم مباشرة إلى TMemoryStream متجه إلى تخزين ثنائي كبير أو استجابة HTTP دون لمس القرص أبدا هناك حافة حادة واحدة يجب تذكرها SaveAs(Stream) يكتب من الموضع الحالي للدفق ولا يعيده إلى البداية بعد ذلك، لذا اضبط Position := 0 بنفسك قبل تسليم الدفق لأي جهة توصله، وإلا قرأ المستهلك صفر بايت واجهة XLS تضيف مقبضين خاصين بها SetTempDir يوجّه الملفات المؤقتة لكاتب BIFF إلى مجلد لديه المساحة وهامش الإدخال والإخراج لاستيعابها، وهو أمر مهم على خوادم يقع فيها المسار المؤقت الافتراضي على قرص نظام ضيق UseSharedFormulas يطوي أجسام الصيغ المتكررة في مجموعات مشتركة، تخفيض حقيقي في الحجم لشكل التقرير الكلاسيكي حيث تُنسخ صيغة واحدة أسفل عمود بأكمله
حلقة الدفعة نفسها تبقى مملة عمدا
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // نسخة جديدة: لا تسرب حالة
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // ملف مدخل واحد سيئ يجب ألا يقتل الدفعة
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
نسخة مصنف جديدة لكل ملف تكلّف ميكروثانيات وتزيل فئة كاملة من أخطاء التلوث بين الملفات: الأنماط والأسماء المعرَّفة وخصائص المستند من الملف 17 ليس لها مسار للتسرب إلى الملف 18 التخطي والمتابعة عند فشل Open يبرر وجوده بنفس القدر، لأن رفعا واحدا مقطوعا في دفعة من 600 ملف يجب أن يكلفك سطر سجل واحد لا بقية التشغيل يستحق الإشارة أيضا إلى ما لا يفعله مسار CSV عمدا SaveAsCSV يكتب الصيغ كنص حرفي ولا يحسبها أبدا، لذا دفعة تحويل يتوقع مستهلكوها أرقاما محسوبة يجب أن تشغّل Calculate على الخلايا المعنية أولا، أو تبدأ من مصنفات تحمل بالفعل نتائج مخبأة من حساب سابق
نموذج التزامن: مصنف واحد لكل خيط
لا كائنات أي من الواجهتين آمنة للخيوط، والتصميم لم يدّعِ خلاف ذلك أبدا لأنه لا توجد حالة عامة مشتركة بين النسخ، قاعدة التوسع ببساطة هي مصنف واحد لكل خيط عامل، دون مشاركة مصنف عبر الخيوط تجمّع من N عامل، كل منهم يملك TXLSXWorkbook خاصا به، يتوسع بشكل قريب من الخطي حتى تصبح الذاكرة السقف، وذلك السقف شيء يمكنك وضع رقم له: أكبر نموذج خلايا متزامن مضروبا في عدد العمال، زائد أي عبء وقت حفظ سطّحته StreamingWrite حين يتعمق الطابور، طبّق ضغطا مضادا عند طابور المهام لا داخل الكاتب خيط جائع كتب نصف مصنف لم ينتج شيئا مفيدا، بينما مهمة انتظرت بضع ثوان لعامل حر تكتمل سليمة
للصورة الأوسع لضبط الأداء، بما في ذلك الصيغ المشتركة، وتخطي الرسوم في جانب القراءة، والمقابض الخاصة بـXLS، راجع دليل أداء المصنفات الكبيرة مهام الدفعات التي تأتي صفوفها مباشرة من استعلام مغطاة بشكل منفصل في أنماط تصدير قاعدة البيانات لتقارير Delphi
يُترجَم HotXLS إلى خدمة Delphi أو C++Builder الخاصة بك ككود Object Pascal أصلي دون اعتماديات خارجية؛ الإصدارات والترخيص على صفحة منتج HotXLS Delphi Component