บทความเทคนิค

การอ่านไฟล์ 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 เริ่มนับจากศูนย์ ในขณะที่ทั้งคอลเลกชันชีตและการเข้าถึงเซลล์เริ่มนับจากหนึ่ง ซึ่งเป็นความไม่สอดคล้องกันที่มีมาก่อน 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 มีซับสตรีมชีตเพียงหนึ่งเดียวพอดี สมุดงานแบบหลายชีตไม่มีอยู่จนกว่ารูปแบบไฟล์จะได้คอนเทนเนอร์ของมัน

เส้นทางการย้ายข้อมูลเชิงปฏิบัติจึงเป็นสองขั้นตอน อ่านไฟล์เก่าเพื่อเอาค่าของมัน แล้วเขียนสมุดงานสมัยใหม่ที่พกค่าเหล่านั้นพร้อมรูปแบบที่คุณกำหนดเอง การอ่านไฟล์เก่า การเขียนแบบสมัยใหม่ และทุกอย่างในระหว่างนั้นทำงานอยู่ในไลบรารีเดียวกันสำหรับ Delphi และ C++Builder อธิบายไว้ที่หน้าคอมโพเนนต์สเปรดชีต HotXLS สำหรับ Delphi