Bài viết kỹ thuật

Chu trình đọc-ghi XLSX không mất mát trong Delphi: Theme, extLst, calcChain

HotXLS, thư viện Excel gốc cho Delphi và C++Builder, được thiết kế để hỗ trợ chu trình đọc-ghi XLSX không mất mát (lossless round-trip): mở một sổ làm việc, thay đổi giá trị một ô, lưu lại, và toàn bộ giao diện tùy chỉnh (theme) của khách hàng, các khối mở rộng extLst lạ, cùng chuỗi tính toán (calculation chain) đều được giữ nguyên vẹn. Ba cơ chế giúp làm được điều này — sao chép nguyên trạng xl/theme/theme1.xml, tuần tự hóa lại dựa trên sự kiện cho các khối <ext> chưa biết, và tạo mới tệp xl/calcChain.xml chuẩn đặc tả mỗi khi lưu sổ làm việc chứa công thức

Tình huống thúc đẩy cả ba cơ chế này rất phổ biến trong thực tế. Một dịch vụ lập hóa đơn tải một mẫu tệp do khách hàng thiết kế trong Excel — có tông màu chủ đạo của doanh nghiệp, các biểu đồ mini (sparklines) trong cột KPI, một quy tắc định dạng có điều kiện được tạo từ một phiên bản Excel mới hơn — viết một giá trị tổng hóa đơn vào ô B3, rồi lưu lại. Khách hàng mở tệp kết quả và thấy các màu sắc thương hiệu đã biến mất, quay về màu xanh Office mặc định, các biểu đồ mini biến mất, và Excel hiện cảnh báo đề xuất "sửa" (repair) tệp tin. Không có dòng mã nào của bạn tác động đến các tính năng đó. Thư viện đã làm điều đó, chỉ bằng việc lưu tệp

Tại sao các tệp Excel bị mất định dạng sau khi chỉnh sửa bằng thư viện?

Các tệp Excel bị mất định dạng sau khi chỉnh sửa bằng thư viện vì hầu hết các thư viện không trực tiếp sửa đổi tệp tin — chúng xây dựng lại nó. Một gói dữ liệu .xlsx thực chất là một tệp nén ZIP chứa các phần XML: xl/workbook.xml, mỗi tệp xl/worksheets/sheetN.xml cho từng trang tính, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml và các thành phần khác. Một thư viện thông thường sẽ phân tích các thành phần đó thành một mô hình đối tượng khi mở và tái tạo lại toàn bộ từ mô hình đó khi lưu. Bất kỳ tính năng nào mà mô hình đối tượng không hỗ trợ biểu diễn — ví dụ một theme nó chưa từng phân tích cú pháp, hoặc một khối mở rộng từ một phiên bản Excel mới hơn — sẽ không có chỗ lưu trữ trong bộ nhớ, do đó thành phần được tạo lại sẽ âm thầm bỏ qua chúng

Chuẩn ECMA-376 đã lường trước một nửa vấn đề này. SpreadsheetML định nghĩa extLst (ECMA-376 Phần 1, "Vùng lưu trữ dữ liệu tính năng tương lai", mục §18.2.10 đối với phần tử cấp sổ làm việc) làm điểm mở rộng được chỉ định: các trình tạo tệp mới hơn sẽ đặt các tính năng mới ở đây, mỗi tính năng được bọc trong một phần tử <ext> mang thuộc tính uri để định danh tính năng đó, và các trình đọc tệp phiên bản cũ hơn được yêu cầu phải bảo toàn những gì chúng không hiểu. Biểu đồ mini, bộ lọc dữ liệu (slicers) và các kiểu định dạng có điều kiện mới hơn đều được lưu trữ theo cách này. Một thư viện làm mất đi các khối <ext> không xác định không chỉ đơn giản là gây mất mát dữ liệu — nó còn vi phạm giao ước tương thích ngược mà định dạng này được thiết kế dựa trên. Câu hỏi thẳng thắn cần đặt ra cho bất kỳ thư viện bảng tính nào bạn đang đánh giá là: nếu tôi thay đổi một ô, những gì khác sẽ bị thay đổi

HotXLS giữ nguyên vẹn từng byte của theme tùy chỉnh như thế nào?

HotXLS bảo toàn giao diện của sổ làm việc bằng cách lưu trữ tạm thời các byte gốc của tệp xl/theme/theme1.xml tại thời điểm mở và ghi lại nguyên trạng khi lưu. Thành phần theme (ECMA-376 Phần 1, §14.2.7) là định dạng DrawingML chứ không phải SpreadsheetML — bao gồm sơ đồ màu sắc, phông chữ, định dạng — và trình tính toán bảng tính không cần phải lập mô hình chi tiết cho nó. Các phiên bản HotXLS trước đây tạo lại một theme Office cố định mỗi khi lưu, dẫn đến lỗi mất màu sắc thương hiệu như đã nêu ở trên; kể từ phiên bản v2.89.46, theme của gói dữ liệu đã mở được lưu ở dạng thô và ghi lại nguyên vẹn, và theme Office mặc định chỉ được tạo ra cho các sổ làm việc mới xây dựng từ đầu. Các byte thô là sự đảm bảo cao nhất cho độ trung thực của dữ liệu: không phân tích cú pháp, không tuần tự hóa lại, không có rủi ro sai lệch dữ liệu

Việc sao chép nguyên trạng được ưu tiên hơn các quyền truy cập theme lập trình. Lớp TXLSXWorkbook cung cấp các thuộc tính ThemeMajorFontThemeMinorFont để bạn chọn phông chữ tiêu đề và nội dung cho sổ làm việc mới, nhưng khi một theme nguyên trạng được thu nhận lúc mở tệp, các trình thiết lập đó sẽ không có tác dụng đối với tệp lưu — chu trình đọc-ghi được ưu tiên hàng đầu. Nếu bạn thực sự cần thay đổi theme của một sổ làm việc có sẵn, đó là dấu hiệu bạn nên chỉnh sửa mẫu trực tiếp trong Excel chứ không phải thông qua một API hướng dữ liệu. Trường hợp sử dụng hàng ngày không cần gọi API nào cho việc này:

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('branded-invoice.xlsx');
    Book.Sheets[0].Cells[3, 2].Value := 42750.00;  // the one edit
    Book.SaveAs('branded-invoice-out.xlsx');
    // theme1.xml in the output is byte-identical to the input
  finally
    Book.Free;
  end;
end;

Điều gì xảy ra với các khối extLst không xác định khi lưu?

HotXLS thu nhận mọi khối <ext> cấp trang tính mà nó không hỗ trợ lập mô hình gốc và ghi lại nó vào mục extLst của trang tính đã lưu, nhờ đó các tính năng được ghi bởi các phiên bản Excel mới hơn vẫn nguyên vẹn qua chu trình đọc-ghi. Kể từ phiên bản v2.131.0, các phân đoạn thu nhận được hiển thị qua thuộc tính chỉ đọc RawWorksheetExts, một đối tượng TStringList trên mỗi trang tính XLSX, giúp khả năng bảo toàn này có thể kiểm chứng được từ mã kiểm thử thay vì chỉ là lời hứa hẹn:

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  i: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('from-newer-excel.xlsx');
    Sheet := Book.Sheets[0];
    WriteLn(Format('%d foreign ext block(s) captured',
      [Sheet.RawWorksheetExts.Count]));
    for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
      WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // peek at each uri
  finally
    Book.Free;
  end;
end;

Chi tiết triển khai cần lưu ý là việc thu nhận được thực hiện dưới dạng tuần tự hóa lại theo cấp độ sự kiện, chứ không phải sao chép byte thô. Trình đọc XML dạng luồng của HotXLS không cung cấp các vị trí byte nguồn, do đó cây con không xác định được xây dựng lại từ các sự kiện Element, Text, và EndElement khi chúng truyền qua. Cách tiếp cận đó tiềm ẩn một cái bẫy kinh điển: một phần tử tự đóng như <a/> chỉ kích hoạt một sự kiện Element được đánh dấu trống chứ không bao giờ kích hoạt sự kiện EndElement, do đó bất kỳ bộ đếm độ sâu nào giảm chỉ dựa trên EndElement sẽ không bao giờ phát hiện được cây con đã đóng. Khi được xử lý đúng, phân đoạn dựng lại sẽ tương đương về mặt ngữ nghĩa với phân đoạn gốc — mặc dù các ký tự bao thuộc tính và định dạng tự đóng được chuẩn hóa nên không giống nhau hoàn toàn từng byte, nhưng Excel đọc hiểu ngữ nghĩa chứ không phải byte thô. Hai thuộc tính của đầu ra từ chính Excel giúp việc ghi lại này an toàn: Excel khai báo các thuộc tính xmlns cần thiết trên phần tử <ext> hoặc bên trong nó, giúp mỗi phân đoạn thu nhận tự chứa đựng không gian tên riêng biệt, và chính tính chất tự chứa đựng này là lý do tại sao việc nhân bản một trang tính bên trong hoặc giữa các sổ làm việc có thể mang theo các khối lạ đi kèm chỉ bằng phép gán danh sách chuỗi thông thường

Ghi tệp calcChain.xml để Excel tin cậy các công thức của bạn

HotXLS ghi tệp xl/calcChain.xml (thành phần Calculation Chain, ECMA-376 Phần 1, §12.3.1) bất cứ khi nào sổ làm việc đã lưu chứa các công thức, và nó chọn lựa giữa hai kiểu sắp xếp thứ tự. Nếu đồ thị phụ thuộc công thức đã được dựng và ở trạng thái mới nhất — bạn đã gọi hàm Recalculate sau lần chỉnh sửa cuối cùng — chuỗi sẽ được xuất ra theo thứ tự topo đầy đủ, các ô tiền lệ đứng trước ô phụ thuộc, với các phần tử tham chiếu vòng được chèn vào ở cuối. Nếu không, các ô sẽ được liệt kê theo thứ tự xuất hiện trong tài liệu. Cả hai cách đều đúng: tài liệu hướng dẫn triển khai định dạng của Microsoft, [MS-XLSX], coi chuỗi tính toán là một gợi ý để Excel tự xác thực và sắp xếp lại khi tải, do đó mọi danh sách đầy đủ đều hợp lệ, và HotXLS chủ động từ chối việc bắt buộc dựng đồ thị bên trong hàm SaveAs — việc xây dựng các cạnh phụ thuộc có chi phí tỷ lệ bậc hai với số lượng ô, một chi phí ẩn không thể chấp nhận được đối với một lượt lưu tệp có hàng triệu ô

Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');

Tại sao lại phải bận tâm về một thành phần mà Excel coi là không bắt buộc? Vì sự vắng mặt của nó là một dấu hiệu. Một số trình đọc tệp — các thuật toán tự sửa lỗi, trình xem của bên thứ ba, công cụ so sánh tệp — mong đợi một sổ làm việc chứa công thức phải mang theo chuỗi tính toán, và một thư viện âm thầm bỏ qua thành phần này khi lưu sẽ tạo ra các tệp tin khác biệt tinh vi so với những gì Excel thực tế ghi ra. Việc xuất ra một chuỗi tính toán hợp lệ giúp giữ cho tệp kết quả luôn nằm trong phạm vi tiêu chuẩn mà phần còn lại của hệ sinh thái phần mềm đã được kiểm thử rộng rãi, đó chính là cốt lõi âm thầm của kỹ nghệ đọc-ghi toàn vẹn

Giới hạn của chu trình đọc-ghi không mất mát

Sự trung thực quan trọng hơn những lời quảng cáo, vì vậy các giới hạn thực tế cần được nêu rõ ràng. HotXLS không sao chép toàn bộ gói dữ liệu theo dạng byte-đối-byte: tệp XML của trang tính, kiểu dáng, chuỗi dùng chung (shared strings) và các thành phần của sổ làm việc đều được tạo lại từ mô hình đối tượng đã giải nghĩa, do đó kết quả đầu ra là trung thực về ngữ nghĩa nhưng không giống hoàn toàn về nhị phân — chỉ riêng các tiêu đề cục bộ của ZIP đã mang nhãn thời gian DOS mới. Các phân đoạn <ext> thu nhận được tái tạo ở dạng chuẩn hóa như mô tả ở trên. Các phông chữ theme được ghi đè bằng chương trình sẽ bị bỏ qua khi một theme nguyên trạng tồn tại. Và lưới bảo toàn có các mắt lưới xác định: các tính năng HotXLS hỗ trợ gốc (ví dụ biểu đồ mini được phân tích cú pháp và ghi lại chứ không phải sao chép mù quáng) cộng với nội dung extLst lạ cộng với các thành phần được lưu đệm nguyên trạng. Một phần không được mô tả cũng không nằm trong điểm mở rộng — ví dụ thành phần tùy chỉnh của một phần mở rộng lạ (add-in) — sẽ nằm ngoài ba cơ chế được đề cập trong bài viết này, do đó bạn hãy tự kiểm thử các mẫu tệp thực tế của mình thay vì giả định

Các công việc bảo toàn liên quan khác sẽ hoàn thiện bức tranh tổng thể. Các dự án VBA và các tham chiếu sổ làm việc bên ngoài đi qua lệnh lưu dựa trên cùng triết lý giữ-lại-những-gì-không-lập-mô-hình, được trình bày trong bài viết đi kèm về bảo toàn VBA và liên kết ngoài, và các thuộc tính tài liệu trong thư mục docPropsAPI đọc-ghi riêng của chúng thay vì bị âm thầm loại bỏ. Khi bạn đánh giá bất kỳ thư viện bảng tính nào, hãy chạy thử nghiệm một ô duy nhất: mở một sổ làm việc thực tế giàu tính năng, thay đổi một giá trị duy nhất, lưu lại và so sánh (diff) các phần đã giải nén so với tệp gốc. Những gì thay đổi nằm ngoài trang tính bạn đã chạm vào sẽ tiết lộ nhiều điều về thư viện đó hơn là bất kỳ bảng so sánh tính năng nào

Các cơ chế đọc-ghi toàn vẹn được mô tả ở đây — bao gồm giữ nguyên trạng theme kể từ phiên bản v2.89.46, thu nhận các khối extLst lạ và xuất tệp calcChain.xml từ phiên bản v2.131.0 — đi kèm trong sản phẩm hiện tại HotXLS Delphi Excel Component, với trang sản phẩm cung cấp tài liệu chi tiết toàn bộ tính năng đọc-ghi XLSX cho Delphi và C++Builder