تعمل مهمة إعداد التقارير بشكل جيد لمدة عام. حيث تقوم بإنشاء مصنف، وتعبئة ورقة عمل بكل ما يعود به الاستعلام، وتقوم بحفظه. ثم يطلب عميل لديه تاريخ يمتد لخمس سنوات تصديرًا كاملاً، ويتجاوز عدد الصفوف مليونًا، وتتوقف العملية بخطأ نفاد الذاكرة قبل وقت طويل من وصول الملف إلى القرص. لم يكن هناك خطأ في التعليمات البرمجية. لقد كانت تحتفظ بالمصنف بأكمله في ذاكرة الوصول العشوائي (RAM) حتى تتمكن من إجراء التسلسل له في النهاية، ونمت الذاكرة التي تحتاجها بشكل متزامن مع عدد الصفوف التي طُلب منها كتابتها
الحل ليس في جهاز أكبر. بل في نموذج كتابة مختلف. يقوم الكاتب المباشر المتدفق في HotXLS بإصدار حزمة OOXML بشكل تدريجي مع وصول الصفوف، وبالتالي فإن الذاكرة التي يستخدمها لا تعتمد على عدد الصفوف التي تكتبها. إنه النظير في جانب الكتابة للقارئ المتدفق: حيث يسير القارئ عبر ورقة ضخمة دون بناء شجرة خلايا، وينتج الكاتب واحدة دون بناء شجرة خلايا أيضًا
لماذا ينمو مسار الحفظ العادي مع البيانات
يقوم مسار TXLSXWorkbook العادي ببناء نموذج كائن كامل أولاً. تعيش كل خلية، مع قيمتها ونوعها ومرجع النمط الخاص بها، ككائن في الذاكرة حتى تستدعي الحفظ، وعند هذه النقطة يتم تسلسل الشجرة بأكملها في الحزمة. هذا النموذج هو النموذج الصحيح عندما تريد قراءة ورقة عمل وتحريرها وإعادة حسابها وكتابتها مرة أخرى، لأن الوصول العشوائي إلى أي خلية هو بالضبط ما يحتاجه التحرير. إنه النموذج الخاطئ عندما تقوم بصب الصفوف في اتجاه واحد ولا تنظر إلى الوراء أبدًا، لأنك تدفع للاحتفاظ بكل صف مقيمًا دون أي فائدة. مليون صف من الكائنات هو مليون صف من الكائنات سواء قمت بزيارتها مرة أخرى أم لا
يزيل الكاتب المتدفق الشجرة. بمجرد كتابة خلية، تصبح بايتات في جزء ورقة العمل، ويتم تسليم هذه البايتات إلى مخرجات zip. تدفق ورقة العمل هو المخزن المؤقت الوحيد الذي ينمو، وهو ينمو على جانب الإخراج، وليس ككائنات دلفي الحية على الكومة (heap). ما يبقى مقيمًا هو مقدار ثابت من مسك الدفاتر (bookkeeping): أسماء الأوراق، وبعض العلامات، ورقم الصف الحالي، وعداد الخلايا. لا تتغير هذه المجموعة بين الصف الأول والصف عشرة ملايين
جدول النصوص المشتركة هو الفخ، والنصوص المضمنة هي المخرج
معظم كتاب XLSX المتدفقين يؤدون بشكل جيد حتى يواجهوا نصًا. يخزن تنسيق OOXML عادةً النصوص في جدول نصوص مشتركة: تتم كتابة كل نص مميز مرة واحدة في جزء منفصل، وكل خلية تحتوي على هذا النص تحمل فهرسًا إلى الجدول بدلاً من النص. إنه تحسين جيد للمساحة للملفات المليئة بالتسميات المتكررة، وهو الافتراضي الذي يستخدمه مسار الحفظ القياسي. المشكلة بالنسبة للكاتب المتدفق قاسية. لإلغاء التكرار، يجب أن يظل الجدول مقيمًا طوال المهمة بأكملها، لأن أي صف لم يأت بعد قد يكرر نصًا من صف مكتوب بالفعل، وفقط خريطة كاملة في الذاكرة للنصوص التي تمت رؤيتها يمكنها تعيين الفهرس الصحيح. لذا فإن البنية الوحيدة التي لا يستطيع الكاتب المتدفق دفقها هي نفس البنية التي يُفترض أن تجعل الملف صغيرًا. البيانات الثقيلة بالنصوص تهزم التدفق الذي جئت من أجله
يتجنب الكاتب المباشر الجدول تمامًا. تتم كتابة النصوص بشكل مضمن، كخلايا t="inlineStr" التي يجلس نصها مباشرة داخل الخلية مع عنصر <is><t>. لا يوجد جدول للتجميع ولا خريطة للنصوص التي تمت رؤيتها للاحتفاظ بها، لذا فإن أعمدة النصوص لا تكلف ذاكرة أكثر من الأعمدة الرقمية. المقايضة صريحة وتستحق البيان بوضوح. تكرر النصوص المضمنة نفس النص أينما حدث، لذا فإن الملف الذي يحتوي على العديد من التسميات المتطابقة يكون أكبر على القرص من مكافئ النص المشترك. أنت تنفق حجم الملف لشراء ذاكرة ثابتة. بالنسبة لتصدير التمرير الواحد، هذا هو الجانب الصحيح من المقايضة، ويمتص ضغط zip الكثير من التكرار في طريق الخروج على أي حال
جدول الأنماط يصل في النهاية، مع تنسيق تاريخ واحد
تقدم الأنماط نفس التوتر الذي تقدمه النصوص. يشير المصنف إلى تنسيقه من خلال جزء الأنماط، ولا يمكن للكاتب المتدفق الاحتفاظ بلوحة متنامية من الأنماط متزامنة مع الخلايا التي قام بمسحها (flushed) بالفعل. يجيب الكاتب المباشر على هذا من خلال إبقاء جدول الأنماط صغيرًا وثابتًا، وإصداره عند الإغلاق بدلاً من إصداره مقدمًا. يغطي تنسيق خلية افتراضي واحد الخلايا العادية. يغطي تنسيق رقم تاريخ واحد التواريخ، مسجلاً برمز تنسيق yyyy-mm-dd في موضع معروف في قائمة تنسيقات الخلايا
تنسيق التاريخ هذا هو السبب في وجود WriteDateTime كاستدعاء خاص به. لا يحتوي Excel على نوع تاريخ أصلي؛ التاريخ هو رقم يرتدي تنسيق تاريخ. يكتب WriteDateTime القيمة كرقم تسلسلي عادي ويضع علامة على الخلية بنمط التاريخ الواحد، بحيث يعرضها جدول البيانات كتاريخ بدلاً من عدد صحيح مكون من خمسة أرقام. التسلسل الذي يكتبه مهم للرحلة ذهابًا وإيابًا (round-tripping). يقوم بتخزين قيمة TDateTime مباشرة تحت نظام تاريخ 1900، وهو نفس العرف الذي يستخدمه مسار حفظ TXLSXWorkbook العادي. نظرًا لأن كلا المسارين يتفقان على التسلسل، فإن الملف الذي ينتجه الكاتب المتدفق يُقرأ مرة أخرى من خلال قارئ HotXLS ويُفتح في Excel بتواريخ تتطابق مع ما قصدته، دون مفاجأة الفارق بواحد (off-by-one) أو الحِقبة (epoch) بين الكاتب والقارئ
الترتيب إلزامي، لأن البايتات قد ذهبت بالفعل
يشتري التدفق ملف تعريف الذاكرة الخاص به بقاعدة واحدة يجب عليك احترامها. يتم إصدار المخرجات أثناء تقدمك ولا يمكن زيارتها مرة أخرى، لذلك يجب كتابة كل شيء بالترتيب الذي يظهر به في الملف. داخل الصف، تذهب الخلايا بترتيب عمودي تصاعدي. داخل الورقة، تذهب الصفوف بترتيب تصاعدي. لا يوجد مخزن مؤقت يتيح للكاتب فرز خلاياك بعد الحدث، لأن الصف الذي أغلقته للتو أصبح بالفعل بايتات في تدفق zip ولم يعد قابلاً للوصول. سلمه العمود 5 ثم العمود 2 في نفس الصف وسيكون الإخراج مشوهًا، لأن الكاتب ببساطة يصدر ما تعطيه إياه بالتسلسل الذي تعطيه إياه
تحتوي واجهة برمجة تطبيقات (API) الصف على ميزة راحة صغيرة للحالة الشائعة. يأخذ AddRow فهرس صف يعتمد على 1 (1-based)، ولكن تمرير 0 يعني أخذ الصف التالي بعد الصف السابق، وبالتالي لا يضطر التعبئة التسلسلية إلى تتبع وتمرير عداد متزايد. يغلق كل AddRow الصف الذي يسبقه، ويغلق كل AddSheet الورقة التي تسبقه، لذلك لا تنهي أبدًا بشكل صريح صفًا أو ورقة. تبدأ التالي ويقوم الكاتب بوضع اللمسات الأخيرة على البنية المفتوحة لك
يتم التعامل مع الهروب (Escaping) حيث يدخل النص في XML
أي نص تكتبه يصبح جزءًا من مستند XML، لذا يجب الهروب من (escape) كيانات XML الخمسة المحددة مسبقًا وإلا فستكون الحزمة غير صالحة في اللحظة التي تحتوي فيها القيمة على علامة العطف (&) أو قوس زاوية. يهرب الكاتب من &، <، >، "، و ' لك على كل من نص السلسلة المضمنة ونص الصيغة، وهما المكانان اللذان تهبط فيهما الأحرف المقدمة من المتصل داخل العلامات. أنت تمرر WideString خامًا ويجعله الكاتب آمنًا. اسم منتج مثل Smith & Co <Ltd> أو صيغة تشير إلى اسم ورقة مقتبس تخرج بصيغة XML جيدة التكوين دون أي هروب من جانبك
دورة الحياة، ولماذا لا يزال Destroy يغلق
إنهاء الحزمة هو ما يكتب جزء المصنف، وجزء الأنماط، وأجزاء أنواع المحتوى (content-types) والعلاقات (relationship)، وأخيرًا الدليل المركزي لـ zip. يحدث هذا العمل في Close. الحزمة التي لا يتم إغلاقها أبدًا هي ملف zip غير مكتمل لن يفتحه أي برنامج جداول بيانات، لذا فإن الإغلاق ليس تنظيفًا اختياريًا، بل هو الخطوة التي تجعل الملف صالحًا. للحماية من نسيان استدعاء Close في مسار خطأ، يقوم Destroy بإجراء إغلاق بأفضل جهد (best-effort) إذا كانت الحزمة لا تزال مفتوحة، وبالتالي فإن تحرير (freeing) الكاتب لا يسرب كائن zip الأساسي حتى عندما يتخطى استثناء الاستدعاء الصريح. النمط الموثوق به هو دائمًا نمط دلفي العادي: اكتب داخل try، استدعِ Close، وحرر (free) في finally
تدفق ورقة كبيرة من البداية إلى النهاية
شكل المهمة هو البدء، إضافة ورقة، صب الصفوف، والإغلاق. يكتب المثال أدناه صف رأس ثم تشغيلًا طويلاً لصفوف بيانات مكتوبة (typed)، ويمزج بين النصوص، والأرقام، وصيغة بدون نتيجة مخبأة (cached)، وتاريخ. الذاكرة التي يستخدمها لعشرة صفوف ولعشرة ملايين صف هي نفسها، لأن كل خلية تغادر إلى تدفق zip بمجرد كتابتها
uses
lxDirectWrite;
procedure StreamReport(const Path: string; RowCount: Integer);
var
W: TXLSDirectWriter;
I: Integer;
begin
W := TXLSDirectWriter.Create;
try
W.BeginFile(Path);
W.AddSheet('Sales');
// Header row, written in ascending column order
W.AddRow(1);
W.WriteString(1, 'Item');
W.WriteString(2, 'Qty');
W.WriteString(3, 'Price');
W.WriteString(4, 'Total');
W.WriteString(5, 'Date');
// Data rows; pass 0 to AddRow to take the next row automatically
for I := 1 to RowCount do
begin
W.AddRow(0);
W.WriteString(1, 'Item ' + IntToStr(I));
W.WriteNumber(2, I);
W.WriteNumber(3, 1.5 + (I mod 10));
W.WriteFormula(4, Format('B%d*C%d', [I + 1, I + 1]));
W.WriteDateTime(5, EncodeDate(2026, 1, 1) + I);
end;
W.Close; // finalises the package
finally
W.Free;
end;
end;
الورقة الثانية هي ببساطة AddSheet أخرى قبل المتابعة، ويغلق الكاتب الورقة الأولى عندما يفتح الورقة الثانية. تستخدم الأعلام المنطقية (Boolean flags) التابع WriteBoolean، والذي يكتب خلية منطقية (boolean) مكتوبة (typed) بدلاً من النص "True". إذا كنت تريد التأكد من أن الملف سليم ويدعم الرحلة ذهابًا وإيابًا (round-trips)، فإن الخاصية CellCount تبلغ عن عدد الخلايا التي تمت كتابتها، وقراءة النتيجة مرة أخرى باستخدام القارئ المتدفق يجب أن يبلغ عن نفس المجموع
// A second sheet of typed flags after the data sheet above
W.AddSheet('Flags');
W.AddRow(1);
W.WriteString(1, 'Name');
W.WriteString(2, 'Active');
W.AddRow(0);
W.WriteString(1, 'alpha');
W.WriteBoolean(2, True);
WriteLn(Format('wrote %d cells', [W.CellCount]));
الكتابة إلى تدفق بدلاً من ملف هي نفس التعليمات البرمجية مع BeginStream بدلاً من BeginFile، مما يتيح للخادم إرسال المصنف إلى استجابة HTTP أو تدفق ذاكرة بدون ملف مؤقت على القرص. لا يمتلك الكاتب التدفق الذي تمرره، لذلك تحتفظ بالتحكم في دورة حياته
عندما يكون العمل عبارة عن نقطة نهاية خادم تبني مصنفات عند الطلب، فإن الأنماط في الكتابات المتدفقة للخادم ومهام الدفعات توضح كيفية ربط هذا بمعالج الطلبات والتصدير المجدول. عندما يكون السؤال هو التكلفة الأوسع للمصنفات الكبيرة جدًا، قراءة وكتابة، فإن أداء المصنفات الكبيرة في دلفي يغطي أين يذهب الوقت والذاكرة في الواقع. يتم شحن الكاتب المباشر المتدفق كجزء من مكون HotXLS لـ دلفي و C++Builder، جنبًا إلى جنب مع واجهات برمجة تطبيقات القراءة والتحرير والحفظ الكاملة التي يتم تغطيتها في مكان آخر في هذه المدونة