مقال تقني

قراءة ملفات Excel من الإصدار 2.0 إلى 4.0 في Delphi باستخدام HotXLS

يفتح HotXLS مصنّفات كتبتها إصدارات Excel 2.0 و3.0 و4.0 مباشرة من Delphi وC++Builder. وهذه الملفات تسبق حاوية المستند المركّب OLE التي يستخدمها كل ملف .xls لاحق، لذا فهي تدفقات سجلات BIFF خام بلا أي غلاف تخزين إطلاقًا، وقارئ مبني لـ BIFF8 لن يجد بنية واحدة قابلة للتعرف بداخلها. وفتح أحدها يستخدم استدعاء Open نفسه المستخدَم لأي مصنّف آخر؛ إذ يكتشف القارئ الصيغة ويبدّل المسارات

وهذه الملفات لا تزال تظهر، وهذا هو السبب الوحيد الذي يجعل كل هذا مهمًا. فالأرشيفات الهندسية، والاحتفاظ بالسجلات الحكومية، وبيانات المختبرات من أجهزة كُتب برنامج التحكم بها في عام 1993، وأنظمة محاسبية طويلة العمر، كلها تركت مصنّفات BIFF2 وBIFF4 وراءها. ويرفض Excel الحديث فتح عدد منها صراحة، بعد أن أزال محوّلات الصيغ القديمة لأسباب أمنية، ما يترك مجموعة بيانات لا يستطيع أحد قراءتها بأداة يملكها أحد

ما الذي يجعل المصنّف السابق لـ OLE مختلفًا؟

كل ملف .xls من Excel 5.0 فصاعدًا هو ملف مركّب OLE2، أي نظام ملفات صغير داخل ملف، مع وجود المصنّف في تدفق باسم Workbook أو Book. ويبدأ تحليل أحدها بتحليل تلك الحاوية، كما هو موصوف في صيغة الملف المركّب الثنائية في Pascal

ولا تملك BIFF2 وحتى BIFF4 أي حاوية. يبدأ الملف فورًا بسجل BOF، ويُرمّز رقم سجل BOF ذاك الجيل: $0009 لـ BIFF2، و$0209 لـ BIFF3، و$0409 لـ BIFF4. ويتحقق HotXLS من طول جسم BOF، الذي يتراوح بين أربعة وستة بايتات، ومن نوع التدفق الفرعي، $0010 لورقة عمل، و$0020 لمخطط بياني، و$0040 لورقة ماكرو، قبل الالتزام بالمسار الخام. وهذا التحقق هو ما يمنع تفسير ملف تالف أو مُحدَّد الهوية خطأً كمصنّف قديم جدًا

ثلاثة أجيال، ثلاث تخطيطات للسجلات

وسجلات الخلايا هي الموضع الذي تتباعد فيه الأجيال بأوضح شكل. تشغل BIFF2 كتلة متجاورة من أرقام سجلات منخفضة، من $0001 إلى $0005 للخلايا الفارغة والصحيحة والرقمية والتصنيفية والمنطقية-أو-الخطأ، ويحمل كل جسم حقل سمة من ثلاثة بايتات حيث تضع الإصدارات اللاحقة فهرس تنسيق موسَّع. وتتخلى BIFF3 وBIFF4 عن ذلك وتعيدان استخدام أرقام سجلات BIFF5 وتخطيطاتها، $0201 و$0203 و$0204 و$0205، بفهرس XF من بايتين

وهذا التفصيل الأخير يسبب فشلًا محددًا يسهل تشخيصه خطأً. فسجل LABEL في BIFF3 أو BIFF4 مطابق بنيويًا لنظيره في BIFF5: الصف والعمود يليهما فهرس التنسيق ثم عدد الأحرف. واكتب قارئًا يفترض تخطيط BIFF2 وسيقرأ بايتين أقل مما ينبغي، ثم يتجاوز نهاية السجل ويُسيء تفسير كل ما يليه. والعرض ليس استثناءً؛ بل مصنّف يُقرأ ويحتوي على بيانات مهملة معقولة الشكل

وتشغل سجلات الصيغ ترقيمًا مماثلًا عبر الأجيال الثلاثة جميعًا، $0006 و$0206 و$0406. وحين تُنتج صيغة نتيجة نصية، تصل تلك السلسلة النصية في سجل منفصل تالٍ، $0007 أو $0207، وصيغة BIFF2 منه تستخدم بادئة طول من بايت واحد بدلًا من البايتين المستخدَمين لاحقًا

لماذا تعود الصيغ كقيم، لا كنص

يقرأ HotXLS النتيجة المخزَّنة مؤقتًا لصيغة في هذه الملفات ولا يحاول إعادة بناء تعبير الصيغة. وهذه حدود متعمَّدة، لا ثغرة تنتظر السد

ويستخدم التعبير المُحلَّل في BIFF2 إلى BIFF4 ترميز رموز يختلف عن BIFF5 وما بعده بطرق تتجاوز الشكليات: فأطوال الرموز مُسبَقة بشكل مختلف، ورموز المراجع لها أحجام مختلفة، وأُعيد ترقيم جداول فهارس الدوال بين الأجيال. وتمرير تلك البايتات عبر مترجم تعبير BIFF8 لا ينتج صيغة خاطئة، بل ينتج صيغة عشوائية. وقراءة القيمة المخزَّنة مؤقتًا تمنحك الرقم أو السلسلة النصية التي حسبها Excel آخر مرة، وهو بالضبط ما يحتاجه ترحيل أرشيف

وتقيم القيمة المخزَّنة مؤقتًا عند إزاحة تعتمد على الجيل داخل السجل: البايت 7 لـ BIFF2 والبايت 6 لـ BIFF3 وBIFF4. وتُرمَّز القيم الخاصة، أي السلاسل النصية، والقيم المنطقية، والأخطاء، والخلايا الفارغة، في كلمة علامة قدرها $FFFF مع مُميِّز، وهو الاصطلاح نفسه الذي حافظت عليه أجيال BIFF اللاحقة

فتح أحدها

الكود المستدعي عادي تمامًا، وهذا هو المقصود. ويحدث الاكتشاف داخل Open:

uses
  lxHandle;

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  R, C: Integer;
  V: Variant;
begin
  Book := TXLSWorkbook.Create;
  try
    if Book.Open('archive\1993-inventory.xls') <> 1 then
    begin
      Writeln('unreadable - quarantine for manual review');
      Exit;
    end;
    Sheet := Book.Sheets[1];          // Sheets[] يبدأ ترقيمها من واحد
    for R := Sheet.UsedRange.FirstRow + 1 to Sheet.UsedRange.LastRow + 1 do
      for C := Sheet.UsedRange.FirstCol + 1 to Sheet.UsedRange.LastCol + 1 do
      begin
        V := Sheet.Cells[R, C].Value;
        if not VarIsEmpty(V) then
          Writeln(Format('R%dC%d = %s', [R, C, VarToStr(V)]));
      end;
  finally
    Book.Free;
  end;
end;

لاحظ حساب الفهرس في تلك الحلقة. حدود UsedRange تبدأ من صفر بينما تبدأ مجموعة الأوراق والوصول إلى الخلايا من واحد، وهو تضارب يسبق واجهة البرمجة الحالية ومحفوظ للتوافق. ونسيان هذا التعديل يدقق المستطيل الخاطئ ولا يُبلغ عن أي شيء غير عادي أثناء ذلك. وتُغطّى الفحوصات المسبقة الرخيصة التي تتجنب تحميل ملف بالكامل في فحص المصنّف الخفيف

ما لا تحصل عليه، وما ينبغي فعله حياله

لا يُفسَّر التنسيق. فـ HotXLS لا يُحلّل سجلات XF وFONT الخاصة بهذه الأجيال، لذا فإن الخطوط، والألوان، والحدود، وتنسيقات الأرقام غير متاحة، والخلايا التي كان Excel يعرضها يومًا كتواريخ تعود كأرقامها التسلسلية الخام

وهذا الأمر الأخير يحتاج إلى معالجة في كودك أنت لا في القارئ، والسبب صريح: تنسيقات الأرقام في BIFF2 إلى BIFF4 ليست موثوقة بما يكفي لقيادة قرار تاريخ تلقائي. فعمود من أرقام خماسية الخانات قد يكون تواريخ، وقد يكون أرقام قطع غيار. حوّل عمدًا، مستخدمًا نظام تاريخ المصنّف، الذي تُوصَف قواعده في الأرقام التسلسلية للتاريخ، ونظام 1904، وتنسيقات الأرقام:

// قرر لكل عمود، أبدًا لكل قيمة: رقم من خمس خانات يمكن أن يكون
// تاريخًا أو رقم قطعة، والتنسيق القديم لن يخبرك بذلك
if ColumnHoldsDates(C) then
begin
  // يفصل بين نظامَي التاريخ 1462 يومًا، لذا فإن الرقم التسلسلي نفسه
  // يُشير إلى تاريخين يفصل بينهما أربع سنوات. اقرأ النظام من
  // المصنّف بدلًا من افتراض أحدهما
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

وملاحظتان بنيويتان تكملان الصورة. تظهر سجلات حماية كلمة المرور وصفحة الترميز داخل تدفق ورقة العمل الواحد بدلًا من تدفق على مستوى المصنّف، لأنه لا يوجد تدفق على مستوى المصنّف لوضعها فيه، لذا يجب التعرف عليها في سياق ورقة العمل. وملف من BIFF2 إلى BIFF4 يحتوي على تدفق فرعي واحد بالضبط لورقة واحدة؛ فالمصنّفات متعددة الأوراق لم تكن موجودة إلى أن اكتسبت الصيغة حاويتها

لذا فإن مسار الترحيل العملي مسار من خطوتين: اقرأ الملف القديم لقيمه، ثم اكتب مصنّفًا حديثًا يحمل تلك القيم بتنسيق تُطبِّقه بنفسك. وتعمل القراءة القديمة، والكتابة الحديثة، وكل ما بينهما في مكتبة واحدة لـ Delphi وC++Builder، موصوفة على صفحة مكوّن HotXLS لجداول البيانات في Delphi