شغّل تحويلًا دفعيًا على عشرة آلاف جدول بيانات طوال الليل، وبحلول الصباح تعود ثلاثة منها بقيمة False. هذا كل ما يمنحك إياه تشريح الجثة عندما تكون نتيجة الحفظ منطقية ثنائية: عدد الإخفاقات، دون أي شيء عن أي ملف، أو أي ورقة، أو أي من عشرات الأسباب المحتملة كان المسؤول. يستبدل HotXLS، مكوّن losLab الأصلي لـDelphi وC++Builder لملفات Excel، ذلك البت الواحد بتشخيصات مُهيكلة. تعرض واجهة IXLSWorkbookProgress قائمة Diagnostics وحدث OnDiagnostic يبلّغان عن رمز رقمي ثابت، ومستوى شدة، والعملية التي فشلت، والورقة التي حدث فيها ذلك، لكل استدعاء Open وSaveAs وRecalculate
لماذا تفشل نتيجة الحفظ المنطقية الثنائية على نطاق واسع؟
ملف واحد فاشل ليس المشكلة التي تخلقها نتيجة منطقية ثنائية؛ ألف منه هي المشكلة. عندما تُعيد SaveAs شيئًا غير النجاح لثلاثة ملفات من أصل عشرة آلاف، السؤال التالي هو نفسه دائمًا: هل هذه الثلاثة قابلة لإعادة المحاولة، أم تحتاج إنسانًا؟ خطأ إذن على مشاركة شبكية ليس الحادثة نفسها كصيغة لا يستطيع محرك الحساب تقييمها، وكلاهما ليس نفسه كورقة عمل تجاوزت بصمت حد صيغة. مع نتيجة نجاح/فشل فقط للعمل بها، تصبح كل واحدة من هذه تذكرة دعم متطابقة، ويتعيّن على أحدهم فتح كل ملف يدويًا، في Excel، والتحديق فيه حتى يتضح السبب. ذلك الفرز اليدوي هو التكلفة الحقيقية لواجهة برمجية منطقية ثنائية، ويتوسع خطيًا مع حجم الدفعة، وهذه بالضبط الخاصية التي لا تريدها من معالجة الأخطاء
داخل IXLSWorkbookProgress: ما الذي يحمله TXLSDiagnostic
IXLSWorkbookProgress هي الواجهة التي يستخدمها HotXLS للإبلاغ عن كيفية سير عملية وعمّا حدث خطأً بداخلها، والنصفان يتشاركان عقدًا واحدًا لسبب: كلاهما أشياء يحتاج استدعاء طويل الأمد لـOpen أو SaveAs أو Recalculate إلى إيصالها دون إطلاق استثناء في منتصف العملية. نصف التقدم هو OnProgress وOnProgressEx، يُطلقان بمرحلة، وحالة، وزوج حالي/إجمالي. نصف التشخيصات هو ما تدور حوله هذه المقالة: خاصية Diagnostics تُعيد قائمة TXLSDiagnostics، واختصار LastDiagnostic لأحدث إدخال، وحدث OnDiagnostic يُطلَق في اللحظة التي يُنشأ فيها كل سجل TXLSDiagnostic. يحمل كل سجل Code رقميًا، وTXLSDiagnosticSeverity، وTXLSDiagnosticOperation التي أنتجته، وMessage قابلة للقراءة البشرية، وSheetIndex وSheetName، وNativeCode يحفظ أيًّا كانت قيمة الإرجاع منخفضة المستوى التي أطلقت الإدخال
var
Book: TXLSXWorkbook;
Diag: TXLSDiagnostic;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.SaveAs('quarterly-report.xlsx') <> 1 then
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
Writeln(Format('[%d] severity=%d sheet="%s": %s',
[Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
end;
finally
Book.Free;
end;
end;
قراءة Diagnostics بهذه الطريقة تتفوق بالفعل بمفردها على نتيجة منطقية ثنائية، لأن Code وSheetName يحوّلان لغزًا إلى حقيقة محددة قابلة للتصفية. يمتد سجل TXLSDiagnostic أبعد مما يطبعه هذا المثال: توجد RecordId وStreamOffset للتحقيق الجنائي على مستوى البايت داخل تدفق BIFF، وتحمل PartName إدخال zip الخاص بـOOXML، مثل xl/worksheets/sheet3.xml، الذي جاءت منه المشكلة. يستحق المعرفة قبل بناء أدوات حولها: في الإصدار الحالي لا يملأ أي موقع استدعاء تشخيصي مدمج RecordId أو StreamOffset، بحيث يبقى كلاهما عند القيمة الافتراضية للمُنشئ وهي -1، أي "غير قابل للتطبيق" لا "صفر". عامل غيابهما كأمر عادي، لا كخلل في معالجك
محركان، شكل واحد، اختلاف هادئ واحد
يشحن HotXLS محركين خلف نموذج الإبلاغ نفسه، واجهة BIFF8 لملفات .xls القديمة وواجهة OOXML لـ.xlsx، ولا يعرضان IXLSWorkbookProgress بشكل متطابق. TXLSWorkbook، محرك .xls، ينفّذ IXLSWorkbookProgress رسميًا، بحيث يمكن تمريره إلى أي مكان يُتوقَّع فيه ذلك النوع من الواجهات. أما TXLSXWorkbook، محرك .xlsx، فيعرض أعضاء Diagnostics وLastDiagnostic وOnDiagnostic وOnProgress وOnProgressEx نفسها بأسماء وأنواع متطابقة، لكن كفئة عادية لا كتنفيذ رسمي لتلك الواجهة، بحيث لن يفي بمعامل IXLSWorkbookProgress بمفرده. عمليًا نادرًا ما يهم هذا، لأن معظم الشيفرات تعمل على فئة مصنّف عمل ملموسة واحدة في كل مرة، لكنه يعني أنه لا يمكنك كتابة مساعد واحد مُعرَّف النوع كـIXLSWorkbookProgress وتسليمه كائن مصنّف عمل أيًّا من المحركين بالتبادل. فرق الحقل الوحيد الذي ينتج مباشرة عن انقسام الصيغة هو PartName: يملؤها محرك XLSX فقط، لأن OOXML وحده لديه أجزاء zip لتسميتها
ما الذي يجعل رمز تشخيص شيئًا يمكنك التفرع عليه بأمان؟
حقل Code هو الجزء الوحيد من التشخيص الذي يستحق تشفير مقارنة ثابتة مقابله؛ أما Message فلا، لأن النثر بالضبط هو نوع الشيء الذي تُعاد صياغته أو ترجمته أو توسيعه بمزيد من التفاصيل في إصدار لاحق دون أن يعامله أحد كتغيير كاسر. رموز التشخيص المدمجة في HotXLS تُقرأ بالفعل كما لو صُممت واضعة ذلك التمييز في الاعتبار: تتراوح رموز متعلقة بالحفظ من 1000 إلى 1005، وتقع رموز متعلقة بالفتح عند 1100 و1101، ورموز متعلقة بالحساب عند 1200 و1201، ورمز صيغة غير مدعومة عند 1300، مع فجوات متروكة داخل كل نطاق بدلًا من ترقيم الرموز متتاليًا عبرها جميعًا. هذا التباعد هو ما يسمح لمورّد بإضافة نمط فشل جديد وقت الحفظ عند، لنقل، 1006 دون إعادة ترقيم الرموز التي تعتمد عليها جملة switch الخاصة بك بالفعل، ويستحق التحقق منه في أي واجهة تشخيصات قبل الالتزام بالمطابقة على رمز في الإنتاج، لا هذه فقط. احتفظ بفرع افتراضي في منطق التوزيع الخاص بك بصرف النظر عن مدى استقرار الترقيم، لأن أنماط فشل جديدة هي بالضبط ما يستمر محلل أو كاتب متطور في اكتشافه. تقع NativeCode وExceptionClass طبقة أسفل Code لحين احتياجك للتصعيد: تحفظ NativeCode قيمة الإرجاع الكامنة، وهي HRESULT من استدعاء تخزين هيكلي (Structured Storage) من ضمنها، وتسجّل ExceptionClass نوع استثناء Delphi عندما كان متورطًا، وهو عادة كافٍ لفتح طلب دعم دقيق دون إرفاق تتبع مكدس كامل
الشدة والعملية تقرران ما تفعله شيفرتك لاحقًا
الشدة والعملية هما ما يحوّلان تشخيصًا من سطر سجل إلى قرار توجيه. تتراوح TXLSDiagnosticSeverity بين Info، وWarning، وError، وFatal، وتُعلِّم TXLSDiagnosticOperation كل إدخال بالاستدعاء الذي أنتجه: Open، أو Save، أو Calculate، أو Export. المحوران مستقلان بالتصميم: xlsDiagnosticUnhandledException رمز ثابت واحد يُطلَق مع ضبط Operation على أيًّا كان الاستدعاء الذي أطلقه فعليًا، بحيث تجيب Code عن ماذا حدث خطأً بينما تجيب Operation بشكل منفصل عن أين، بدلًا من الحاجة إلى رمز مميز لاستثناء أثناء الفتح مقابل آخر أثناء الحفظ. ذلك التركيب هو أيضًا ما يجعل التوجيه ميكانيكيًا: سجّل تحذيرًا وتابع، حفظ أُلغي عبر علم Aborted مثال نموذجي؛ احسب خطأً وأبقِ الدفعة تعمل، ورقة عمل فشلت في التسلسل مثال نموذجي؛ أوقف الدفعة عند شدة قاتلة، لأن ذلك المستوى يعني أن استثناءً غير معالَج قد فكّك الاستدعاء بالفعل ومتابعة العمل من حالة نصف مُحدَّثة تحمل مخاطرة. تحفظ صادقة واحدة: Info موجودة في التعداد كافتراضي يبدأ به سجل TXLSDiagnostic جديد، لكن كل موقع استدعاء تشخيصي مدمج في إصدار HotXLS اليوم لا يُطلق سوى Warning أو Error أو Fatal؛ Info محجوزة لاستخدام مستقبلي، ليست شيئًا يُصدره المحرك اليوم
// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
case Diag.Severity of
xlsDiagnosticWarning:
Writeln(Format('WARN [%d] %s', [Diag.Code, Diag.Message]));
xlsDiagnosticError:
begin
Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
[Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
Inc(FailedSheetCount);
end;
xlsDiagnosticFatal:
raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
end;
end;
ربط OnDiagnostic بخط أنابيب دفعي
استقصاء Diagnostics بعد كل استدعاء يعمل لملف واحد؛ يتوقف عن العمل بمجرد عودتك إلى تلك الدفعة الليلية من عشرة آلاف ملف، لأن Diagnostics تُمسَح في بداية كل استدعاء Open وSaveAs وRecalculate. اقرأها بعد الملف الثالث في حلقة وسترى فقط تشخيصات الملف الثالث؛ أيًّا كان ما أبلغ عنه الملفان الأولان قد اختفى بالفعل. تحل OnDiagnostic ذلك بتحويل المجموعة إلى تدفق: اشترك مرة واحدة قبل بدء الحلقة، ويُطلَق المعالج نفسه لكل ملف، بالترتيب، مع بقاء اسم الملف في النطاق عبر حقل مثيل
type
TBatchConverter = class
private
FCurrentFile: string;
FFailedFiles: TStringList;
procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
end;
procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
if Diagnostic.Severity >= xlsDiagnosticError then
FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
[FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;
// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
FCurrentFile := FileNames[I];
if Book.Open(FCurrentFile) = 1 then
Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;
ما الذي تكلّفه دالة رد النداء فعليًا
OnDiagnostic رخيصة لسبب بنيوي: فهي لا تُطلَق إلا عندما يكون شيء ما خاطئًا بالفعل، والخطأ نادر مقارنة بعدد الخلايا أو الصفوف أو أوراق العمل التي يحملها مصنّف عمل. قارن ذلك بـOnProgress وOnProgressEx، اللتين تبلّغان عن تقدم روتيني وكان يجب تصميمهما حول تردد الاستدعاء منذ البداية. يُطلق HotXLS تقدمًا على مستوى ورقة العمل مرة واحدة لكل ورقة أثناء Open وSaveAs، لا مرة لكل خلية أو صف، وهذا ما يبقي النفقات العامة لكل استدعاء صغيرة حتى في مصنّفات عمل بملايين الخلايا؛ وتذهب Recalculate أبعد وتُحدّ حدث تقدمها الخاص إلى نحو كل أربعة بالمئة من رسم التبعيات البياني، بحيث تمنحك إعادة حساب كاملة نبضة قلب بدلًا من إغراق خيط واجهتك بالأحداث. لم تحتج التشخيصات أي شيء من ذلك التحديد، لأن عدد الأحداث محدود بعدد المشكلات الفعلية، لا بحجم الملف
المكان الوحيد الذي لا يزال فيه الأداء يعتمد عليك هو داخل المعالج نفسه. تُطلَق OnDiagnostic بشكل متزامن، على الخيط المُشغِّل لـOpen أو SaveAs أو Recalculate، بحيث يصبح معالج يحجب، كتابة متزامنة إلى خدمة تسجيل عن بُعد مثلًا، جزءًا من زمن ذلك الاستدعاء الفعلي. لملف واحد هذا غير مرئي. مضروبًا عبر دفعة من عشرة آلاف ملف، يصبح الفرق بين مهمة تنتهي طوال الليل وأخرى لا تزال تعمل وقت الغداء، لذا خزّن مؤقتًا ما يحتاج المعالج فعله وأفرغه بشكل غير متزامن بدلًا من تنفيذ الجزء البطيء داخليًا
التشخيصات المُهيكلة أكثر قيمة بالضبط حيث تكون النتيجة المنطقية الثنائية أضعف، في سير العمل الذي يمسّ ملفات كثيرة بدلًا من ملف واحد. خط أنابيب تدقيق وتحويل مصنّفات العمل هو أوضح مثال: بدلًا من تسجيل نجاح/فشل مجرد لكل ملف، أرفق قائمة Diagnostics الخاصة بكل ملف بسجل تدقيقه، ويخبرك التقرير ليس فقط ما فشل بل لماذا، وهذا معظم ما تحاول مقالتنا حول بناء منصة تدقيق وتحويل مصنّفات العمل إتقانه أصلًا. الاقتران نفسه بين التقدم والتشخيصات ينتمي أيضًا إلى أي سير عمل يحتاج بالفعل إبلاغ تقدم لحسابه الخاص، وهذا بالضبط المجال المشروح في دليلنا حول أداء مصنّفات العمل الكبيرة في HotXLS، حيث يكون استدعاء Open أو SaveAs الطويل شائعًا بما يكفي بحيث تكون OnProgress موصولة بالفعل وتكون OnDiagnostic إضافة طبيعية شبه مجانية بجانبها
لا شيء من هذا يتطلب تثبيت Excel في أي مكان في خط الأنابيب، ولا شيء منه يتطلب التقاط استثناء عام وتخمين ما كان يعنيه. أعضاء IXLSWorkbookProgress وDiagnostics وLastDiagnostic وOnDiagnostic جزء من مكوّن HotXLS القياسي لـDelphi وC++Builder، إلى جانب مرجع رموز التشخيص الكامل وبقية سطح Open وSaveAs وRecalculate الذي استعرضته هذه المقالة