מאמר טכני

קריאת קובצי Excel 2.0 עד 4.0 בדלפי עם HotXLS

HotXLS פותחת חוברות עבודה שנכתבו על ידי Excel 2.0, 3.0 ו-4.0 ישירות מדלפי ומ-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 מבוססי-אפס בעוד שגם אוסף הגיליונות וגם גישת התאים מבוססי-אחד, חוסר עקביות שקודם ל-API הנוכחי ונשמר לתאימות. שכחת ההתאמה מבקרת את המלבן השגוי ומדווחת שום דבר חריג תוך כדי כך. בדיקות מקדימות זולות שנמנעות מטעינת קובץ בכלל מכוסות בבדיקה קלת-משקל של חוברת עבודה

מה לא מקבלים, ומה לעשות בקשר לזה

עיצוב לא מתפרש. 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 מכיל בדיוק תת-זרם גיליון אחד; חוברות עבודה מרובות-גיליונות לא היו קיימות עד שהפורמט קיבל את המכל שלו

נתיב ההגירה המעשי הוא לכן דו-שלבי: קראו את הקובץ הישן לערכיו, ואז כתבו חוברת עבודה מודרנית שנושאת את הערכים האלה עם עיצוב שאתם מיישמים בעצמכם. קריאה ישנה, כתיבה מודרנית וכל מה שביניהם רצים בספרייה אחת לדלפי ו-C++Builder, המתוארת בעמוד רכיב הגיליון האלקטרוני HotXLS Delphi