Bài viết kỹ thuật

Đọc File Excel 2.0 đến 4.0 trong Delphi bằng HotXLS

HotXLS mở các workbook do Excel 2.0, 3.0 và 4.0 tạo ra trực tiếp từ Delphi và C++Builder. Những file này có trước container tài liệu ghép OLE mà mọi file .xls sau này đều dùng, nên chúng là các luồng bản ghi BIFF thô hoàn toàn không có lớp bọc lưu trữ nào, và một bộ đọc được xây cho BIFF8 sẽ không tìm thấy một cấu trúc nào có thể nhận diện được bên trong chúng. Mở một file như vậy dùng cùng lệnh gọi Open như bất kỳ workbook nào khác; bộ đọc phát hiện định dạng và chuyển sang đường xử lý phù hợp

Những file này vẫn xuất hiện, đó chính là lý do duy nhất khiến tất cả điều này quan trọng. Kho lưu trữ kỹ thuật, lưu trữ hồ sơ chính phủ, dữ liệu phòng thí nghiệm từ các thiết bị có phần mềm điều khiển được viết vào năm 1993, và các hệ thống kế toán chạy lâu năm đều để lại workbook BIFF2 và BIFF4. Excel hiện đại thẳng thừng từ chối mở nhiều file trong số đó, vì đã loại bỏ các bộ chuyển đổi cũ vì lý do bảo mật, khiến một tập dữ liệu không ai có công cụ nào đọc được

Điều gì khiến một workbook tiền-OLE khác biệt?

Mọi file .xls từ Excel 5.0 trở đi đều là file ghép OLE2, một hệ thống file nhỏ bên trong một file, với workbook nằm trong một luồng tên là Workbook hoặc Book. Phân tích một file như vậy bắt đầu bằng việc phân tích container đó, như được mô tả trong định dạng nhị phân file ghép trong Pascal

BIFF2 đến BIFF4 không có container. File bắt đầu ngay lập tức bằng một bản ghi BOF, và số bản ghi của BOF đó mã hóa thế hệ: $0009 cho BIFF2, $0209 cho BIFF3 và $0409 cho BIFF4. HotXLS xác thực độ dài phần thân của BOF, nằm giữa bốn và sáu byte, và loại luồng con, $0010 cho worksheet, $0020 cho biểu đồ và $0040 cho sheet macro, trước khi cam kết theo đường xử lý thô. Việc xác thực đó chính là thứ ngăn một file hỏng hoặc bị nhận diện sai bị hiểu nhầm thành một workbook rất cũ

Ba thế hệ, ba cách bố trí bản ghi

Bản ghi ô là nơi các thế hệ khác biệt rõ nhất. BIFF2 chiếm một khối liên tục các số bản ghi thấp, $0001 đến $0005 cho ô rỗng, số nguyên, số, nhãn và boolean-hoặc-lỗi, và mỗi phần thân mang một trường thuộc tính ba byte trong khi các phiên bản sau đặt chỉ số định dạng mở rộng. BIFF3 và BIFF4 bỏ cách đó và tái sử dụng số bản ghi cùng cách bố trí của BIFF5, $0201, $0203, $0204$0205, với chỉ số XF hai byte

Chi tiết cuối cùng đó gây ra một kiểu lỗi cụ thể và rất dễ chẩn đoán sai. Một bản ghi LABEL của BIFF3 hoặc BIFF4 có cấu trúc giống hệt đối tác BIFF5 của nó, hàng và cột theo sau bởi chỉ số định dạng rồi đến số ký tự. Viết một bộ đọc giả định cách bố trí của BIFF2 và nó sẽ đọc thiếu hai byte, rồi đi lệch ra khỏi cuối bản ghi và diễn giải sai mọi thứ phía sau. Triệu chứng không phải là một ngoại lệ; đó là một workbook đọc được nhưng chứa rác trông có vẻ hợp lý

Bản ghi công thức có cách đánh số song song trên cả ba thế hệ, $0006, $0206$0406. Khi một công thức cho ra kết quả chuỗi, chuỗi đó đến trong một bản ghi theo sau riêng biệt, $0007 hoặc $0207, và dạng BIFF2 của nó dùng tiền tố độ dài một byte thay vì tiền tố hai byte dùng về sau

Vì sao công thức quay lại dưới dạng giá trị chứ không phải văn bản?

HotXLS đọc kết quả đã cache của một công thức trong những file này và không cố dựng lại biểu thức công thức. Đây là một ranh giới có chủ đích, không phải một khoảng trống đang chờ được lấp đầy

Biểu thức đã phân tích trong BIFF2 đến BIFF4 dùng cách mã hóa token khác với BIFF5 trở về sau theo những cách vượt xa yếu tố thẩm mỹ: độ dài token có tiền tố khác nhau, token tham chiếu có kích thước khác nhau, và bảng chỉ số hàm đã được đánh số lại giữa các thế hệ. Cho những byte đó chạy qua một bộ dịch biểu thức BIFF8 không cho ra một công thức sai, nó cho ra một công thức ngẫu nhiên. Đọc giá trị đã cache cho bạn con số hoặc chuỗi mà Excel đã tính lần cuối, đây chính xác là điều một cuộc di chuyển dữ liệu lưu trữ thực sự cần

Giá trị đã cache nằm ở một offset phụ thuộc thế hệ bên trong bản ghi: byte 7 cho BIFF2 và byte 6 cho BIFF3 và BIFF4. Các giá trị đặc biệt, chuỗi, boolean, lỗi và ô rỗng, được mã hóa trong một từ đánh dấu $FFFF kèm bộ phân biệt, cùng quy ước mà các thế hệ BIFF sau này vẫn giữ

Mở một file

Mã gọi không có gì đặc biệt, và đó chính là điểm mấu chốt. Việc phát hiện xảy ra bên trong 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[] đánh số từ 1
    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;

Hãy chú ý phép tính chỉ số trong vòng lặp đó. Giới hạn UsedRange đánh số từ 0 trong khi cả tập hợp sheet lẫn truy cập ô đều đánh số từ 1, một điểm không nhất quán có trước API hiện tại và được giữ lại vì lý do tương thích. Quên điều chỉnh sẽ kiểm tra sai hình chữ nhật và báo cáo không có gì bất thường trong khi làm vậy. Các bước kiểm tra sơ bộ giá rẻ tránh phải tải cả file được đề cập trong kiểm tra workbook dạng nhẹ

Bạn không nhận được gì, và nên làm gì với điều đó

Định dạng không được diễn giải. HotXLS không phân tích bản ghi XF và FONT của các thế hệ này, nên font, màu sắc, viền và định dạng số không khả dụng, và các ô mà Excel từng hiển thị dưới dạng ngày tháng quay lại dưới dạng số serial thô của chúng

Điểm cuối cùng đó cần được xử lý trong mã của riêng bạn thay vì trong bộ đọc, và lý do là thẳng thắn: định dạng số trong BIFF2 đến BIFF4 không đủ tin cậy để đưa ra quyết định ngày tháng tự động. Một cột số năm chữ số có thể là ngày tháng, hoặc có thể là mã số phụ tùng. Hãy chuyển đổi một cách có chủ đích, dùng hệ ngày của workbook, với quy tắc được mô tả trong số serial ngày tháng, hệ 1904 và định dạng số:

// Quyết định theo từng cột, không bao giờ theo từng giá trị: một
// số năm chữ số có thể là ngày tháng hoặc mã số phụ tùng, và định
// dạng cũ sẽ không cho bạn biết
if ColumnHoldsDates(C) then
begin
  // Hai hệ ngày cách nhau 1462 ngày, nên cùng một số serial biểu
  // thị hai ngày cách nhau bốn năm. Đọc hệ từ workbook thay vì
  // giả định một hệ nào đó
  if Book.Date1904 then
    Writeln(DateToStr(SerialToDate1904(V)))
  else
    Writeln(DateToStr(SerialToDate1900(V)));
end
else
  Writeln(VarToStr(V));

Hai lưu ý về cấu trúc hoàn thiện bức tranh này. Bảo vệ mật khẩu và bản ghi trang mã xuất hiện bên trong luồng worksheet đơn lẻ thay vì trong một luồng cấp workbook, vì không có luồng cấp workbook nào để đặt chúng vào, nên chúng phải được nhận diện trong ngữ cảnh worksheet. Và một file BIFF2 đến BIFF4 chứa đúng một luồng con sheet; workbook nhiều sheet không tồn tại cho đến khi định dạng có được container của nó

Vì vậy con đường di chuyển dữ liệu thực tế là hai bước: đọc file cũ để lấy giá trị của nó, rồi ghi một workbook hiện đại mang những giá trị đó với định dạng do chính bạn áp dụng. Đọc file cũ, ghi file hiện đại và mọi thứ ở giữa đều chạy trong một thư viện cho Delphi và C++Builder, được mô tả trên trang thành phần bảng tính HotXLS Delphi