تعمل مهمة إعداد التقارير بشكل جيد لمدة عام. فهي تبني مصنفاً، وتملأ ورقة بما يرجعه الاستعلام، وتحفظه. ثم يطلب عميل لديه تاريخ يمتد لخمس سنوات عملية تصدير كاملة، ويتجاوز عدد الصفوف المليون، وتموت العملية بخطأ نفاد الذاكرة قبل وقت طويل من وصول الملف إلى القرص. لم يكن هناك خطأ في الكود. لقد كان يحتفظ بالمصنف بأكمله في ذاكرة الوصول العشوائي (RAM) حتى يتمكن من تسلسله (serialize) في النهاية، ونمت الذاكرة التي يحتاجها بخطى متزامنة مع عدد الصفوف التي طُلب منه كتابتها
الإصلاح ليس باستخدام آلة أكبر. إنه نموذج كتابة مختلف. يصدر الكاتب المباشر المتدفق (streaming direct writer) في HotXLS حزمة OOXML بشكل تدريجي مع وصول الصفوف، وبالتالي فإن الذاكرة التي يستخدمها لا تعتمد على عدد الصفوف التي تكتبها. إنه النظير في جانب الكتابة للقارئ المتدفق: حيث يمشي القارئ عبر ورقة ضخمة دون بناء شجرة خلايا، ينتج الكاتب ورقة دون بناء شجرة خلايا أيضاً
لماذا ينمو مسار الحفظ العادي مع البيانات
يبني مسار TXLSXWorkbook العادي نموذج كائن كاملاً أولاً. تعيش كل خلية، بقيمتها ونوعها ومرجع نمطها، ككائن في الذاكرة حتى تستدعي حفظ (save)، وعند هذه النقطة يتم تسلسل الشجرة بأكملها داخل الحزمة. هذا النموذج هو النموذج الصحيح عندما تريد قراءة ورقة وتعديلها وإعادة حسابها وكتابتها مرة أخرى، لأن الوصول العشوائي إلى أي خلية هو بالضبط ما يحتاجه التحرير. إنه النموذج الخاطئ عندما تقوم بصب الصفوف في اتجاه واحد ولا تنظر إلى الوراء أبداً، لأنك تدفع مقابل الحفاظ على كل صف مقيماً دون أي فائدة. مليون صف من الكائنات هو مليون صف من الكائنات سواء أعدت زيارتها أم لا
الكاتب المتدفق يزيل الشجرة. بمجرد كتابة خلية، تصبح بايتات في جزء ورقة العمل، ويتم تسليم هذه البايتات إلى مخرجات zip. دفق ورقة العمل هو المخزن المؤقت الوحيد الذي ينمو، وهو ينمو على جانب الإخراج، وليس ككائنات Delphi حية على الكومة (heap). ما يبقى مقيماً هو مقدار ثابت من مسك الدفاتر: أسماء الأوراق، وبضع علامات، ورقم الصف الحالي، وعداد الخلايا. لا تتغير هذه المجموعة بين الصف الأول والصف عشرة ملايين
جدول السلاسل المشتركة هو الفخ، والسلاسل المضمنة هي المخرج
تعمل معظم برامج كتابة XLSX المتدفقة بشكل جيد حتى تقابل نصاً. يخزن تنسيق OOXML السلاسل عادةً في جدول السلاسل المشتركة (shared-string table): تتم كتابة كل سلسلة مميزة مرة واحدة في جزء منفصل، وكل خلية تحتوي على هذه السلسلة تحمل فهرساً في الجدول بدلاً من النص. إنه تحسين جيد للمساحة للملفات المليئة بالتسميات المتكررة، وهو الإعداد الافتراضي الذي يستخدمه مسار الحفظ القياسي. تكمن المشكلة بالنسبة للكاتب المتدفق في أنها قاسية. لإلغاء التكرار، يجب أن يظل الجدول مقيماً طوال المهمة بأكملها، لأن أي صف لم يأتِ بعد قد يكرر سلسلة من صف مكتوب بالفعل، وفقط خريطة كاملة داخل الذاكرة للسلاسل المرئية يمكنها تعيين الفهرس الصحيح. لذا فإن الهيكل الوحيد الذي لا يستطيع الكاتب المتدفق دفقه هو الهيكل ذاته الذي يفترض أن يجعل الملف صغيراً. تهزم البيانات كثيفة النص التدفق الذي جئت من أجله
يتجنب الكاتب المباشر الجدول تماماً. تتم كتابة السلاسل بشكل مضمن، كخلايا t="inlineStr" حيث يوجد نصها مباشرة داخل الخلية باستخدام عنصر <is><t>. لا يوجد جدول لتجميعه ولا توجد خريطة للسلاسل المرئية للاحتفاظ بها، لذلك لا تكلف أعمدة النص ذاكرة أكثر من الأعمدة الرقمية. المقايضة صريحة وتستحق توضيحها. تكرر السلاسل المضمنة نفس النص أينما حدث، لذلك يكون حجم الملف الذي يحتوي على العديد من التسميات المتطابقة أكبر على القرص من نظيره ذو السلاسل المشتركة. أنت تنفق حجم الملف لشراء ذاكرة ثابتة. بالنسبة لتصدير من تمريرة واحدة (one-pass)، هذا هو الجانب الصحيح من المقايضة، ويمتص ضغط zip الكثير من التكرار في الطريق للخروج على أي حال
يصل جدول الأنماط في النهاية، بتنسيق تاريخ واحد
تمثل الأنماط نفس التوتر الذي تمثله السلاسل. يرجع المصنف في تنسيقه إلى جزء أنماط، ولا يمكن للكاتب المتدفق أن يحافظ على لوحة أنماط متنامية متزامنة مع الخلايا التي قام بإفراغها بالفعل. يجيب الكاتب المباشر على ذلك من خلال إبقاء جدول الأنماط صغيراً وثابتاً، وإصداره عند الإغلاق بدلاً من إصداره مسبقاً. يغطي تنسيق خلية افتراضي واحد الخلايا العادية. يغطي تنسيق رقم تاريخ واحد التواريخ، مسجلاً برمز تنسيق yyyy-mm-dd في موضع معروف في قائمة تنسيقات الخلايا
تنسيق التاريخ هذا هو السبب وراء وجود WriteDateTime كاستدعاء خاص به. ليس لدى Excel نوع تاريخ أصلي؛ التاريخ هو رقم يرتدي تنسيق تاريخ. تكتب WriteDateTime القيمة كرقم تسلسلي عادي وتميز الخلية بنمط التاريخ الواحد، لذلك يقوم جدول البيانات بتقديمها كتاريخ بدلاً من عدد صحيح مكون من خمسة أرقام. الرقم التسلسلي الذي يكتبه مهم للذهاب والإياب. يقوم بتخزين قيمة TDateTime مباشرة تحت نظام تاريخ 1900، وهو نفس التقليد الذي يستخدمه مسار حفظ TXLSXWorkbook العادي. نظراً لأن كلا المسارين يتفقان على الرقم التسلسلي، فإن الملف الذي ينتجه الكاتب المتدفق يقرأ مرة أخرى من خلال قارئ HotXLS ويُفتح في Excel بتواريخ تتطابق مع ما قصدته، دون مفاجأة اختلاف بمقدار واحد أو عهد (epoch) بين الكاتب والقارئ
الترتيب إلزامي، لأن البايتات قد اختفت بالفعل
يشتري التدفق ملف تعريف الذاكرة الخاص به بقاعدة واحدة يجب عليك الالتزام بها. يتم إصدار المخرجات أثناء تقدمك ولا يمكن زيارتها مرة أخرى، لذلك يجب كتابة كل شيء بالترتيب الذي يظهر به في الملف. ضمن الصف، تنتقل الخلايا بترتيب الأعمدة التصاعدي. ضمن الورقة، تسير الصفوف بترتيب تصاعدي. لا يوجد مخزن مؤقت يسمح للكاتب بفرز خلاياك بعد الحدث، لأن الصف الذي أغلقته للتو قد أصبح بالفعل بايتات في دفق zip ولم يعد قابلاً للوصول. سلمه العمود 5 ثم العمود 2 في نفس الصف وسيكون الإخراج تالفاً، نظراً لأن الكاتب يصدر ببساطة ما تعطيه إياه بالتسلسل الذي تعطيه إياه
تحتوي واجهة برمجة تطبيقات الصف على راحة صغيرة للحالة الشائعة. يأخذ AddRow فهرس صف قائماً على 1، ولكن تمرير 0 يعني أخذ الصف التالي بعد الصف السابق، لذلك لا يضطر التعبئة المتسلسلة إلى تتبع وتمرير عداد متزايد. كل AddRow يغلق الصف قبله، وكل AddSheet يغلق الورقة قبلها، لذلك لا تنهي صراحةً صفاً أو ورقة. تبدأ في التالي ويقوم الكاتب بوضع اللمسات الأخيرة على الهيكل المفتوح نيابة عنك
يتم التعامل مع الهروب عند إدخال النص إلى XML
يصبح أي نص تكتبه جزءاً من مستند XML، لذلك يجب الهروب من كيانات XML الخمسة المحددة مسبقاً (escaped) أو ستصبح الحزمة غير صالحة في اللحظة التي تحتوي فيها القيمة على علامة العطف أو قوس زاوية. يهرب الكاتب من & و < و > و " و ' نيابة عنك في كل من نص السلسلة المضمنة ونص الصيغة، وهما المكانان اللذان تهبط فيهما الأحرف المقدمة من المتصل داخل الترميز. أنت تمرر WideString خاماً والكاتب يجعله آمناً. يخرج اسم منتج مثل Smith & Co <Ltd> أو صيغة تشير إلى اسم ورقة مقتبس باعتباره XML منسقاً بشكل جيد دون أي هروب من جانبك
دورة الحياة، ولماذا لا يزال Destroy يُغلق
إنهاء الحزمة هو ما يكتب جزء المصنف، وجزء الأنماط، وأنواع المحتوى وأجزاء العلاقات، وأخيراً الدليل المركزي لـ zip. يحدث هذا العمل في Close. الحزمة التي لا يتم إغلاقها أبداً هي ملف zip غير مكتمل لن يفتحه أي برنامج جدول بيانات، لذلك الإغلاق ليس تنظيفاً اختيارياً، إنه الخطوة التي تجعل الملف صالحاً. للحماية من نسيان Close في مسار خطأ، يؤدي Destroy إلى أفضل جهد للإغلاق إذا كانت الحزمة لا تزال مفتوحة، لذا فإن تحرير الكاتب لا يسرب كائن zip الأساسي حتى عندما يتخطى استثناء الاستدعاء الصريح. لا يزال النمط الموثوق به هو النمط العادي لـ Delphi: اكتب داخل try، واستدعِ Close، وحرر (free) في finally
دفق ورقة كبيرة من طرف إلى طرف
شكل المهمة هو البدء (begin)، وإضافة ورقة، وصب الصفوف، والإغلاق. يكتب المثال أدناه صف رأس ثم سلسلة طويلة من صفوف البيانات المكتوبة، وخلط السلاسل والأرقام وصيغة بلا نتيجة مخبأة مؤقتاً وتاريخاً. الذاكرة التي تستخدمها لعشرة صفوف وعشرة ملايين صف هي نفسها، لأن كل خلية تغادر إلى دفق 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 أخرى قبل المتابعة، ويغلق الكاتب الورقة الأولى عندما يفتح الثانية. تستخدم العلامات المنطقية WriteBoolean، التي تكتب خلية منطقية مكتوبة بدلاً من النص "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]));
إن الكتابة في دفق بدلاً من ملف هي نفس الكود مع استبدال BeginFile بـ BeginStream، والذي يتيح للخادم إرسال المصنف إلى استجابة HTTP أو دفق ذاكرة دون ملف مؤقت على القرص. لا يمتلك الكاتب الدفق الذي تمرره، لذلك تحتفظ بالتحكم في عمره الافتراضي
عندما يكون العمل عبارة عن نقطة نهاية خادم تبني مصنفات حسب الطلب، فإن الأنماط الموجودة في الكتابات المتدفقة للخادم والمهام الدفعية توضح كيفية توصيل ذلك بمعالج طلبات وتصدير مجدول. عندما يكون السؤال هو التكلفة الأوسع للمصنفات الكبيرة جداً، من حيث القراءة والكتابة معاً، يغطي أداء المصنف الكبير في Delphi إلى أين يذهب الوقت والذاكرة في الواقع. يتم شحن الكاتب المباشر المتدفق كجزء من HotXLS Component لـ Delphi و C++Builder، إلى جانب واجهات برمجة تطبيقات القراءة والتحرير والحفظ الكاملة التي تمت تغطيتها في مكان آخر في هذه المدونة