Đôi khi câu hỏi duy nhất mà một routine tiếp nhận đầu vào cần trả lời chỉ mang tính cấu trúc: workbook này có sheet nào tên "Mapping" không, hay nó mang bao nhiêu tab. Trả lời câu hỏi đó bằng cách gọi Open là cách làm tốn kém. Một lần mở đầy đủ làm phình to bảng shared string, giải mã từng bản ghi style, và duyệt qua từng ô của mỗi worksheet, bởi vì nó không có cách nào biết rằng bạn chỉ muốn xem mục lục. Trên một tệp lớn, đó là hàng trăm megabyte cấp phát bộ nhớ và vài giây CPU chỉ để đọc một danh sách chiếm vài kilobyte. HotXLS, thư viện bảng tính thuần cho Delphi từ losLab, tự nó cung cấp cho bạn danh sách đó: GetSheetNames trả về tên các worksheet, theo đúng thứ tự trong workbook, mà không cần dựng lên dù chỉ một ô
Vì sao đọc mục lục lại rẻ đến vậy
Cả hai định dạng bảng tính đều đặt mục lục của mình gần đầu tệp, đó chính là lý do khiến một lệnh liệt kê nhanh chứ không phải khéo léo gì cả. Một package OOXML giữ mục lục sheet trong xl/workbook.xml, một phần luôn nhỏ gọn dù workbook chứa mười dòng hay mười triệu dòng. Một tệp .xls BIFF8 lưu các bản ghi BoundSheet ở đầu luồng globals của workbook, trước cả mọi dữ liệu ô. Vậy nên phần công việc mà một lệnh liệt kê tránh được không phải là một sai số làm tròn so với một lần mở đầy đủ. Đó là phần lớn của cả tệp. Đọc mục lục tốn cùng một nhúm kilobyte bất kể số dòng là bao nhiêu, trong khi một lần mở đầy đủ lại co giãn theo dữ liệu, và trên một workbook cỡ nhiều megabyte, khoảng cách đó lên tới nhiều bậc độ lớn, cả về số byte chạm tới lẫn bộ nhớ cấp phát
Cái chi phí phẳng đó chính là đặc tính đáng để thiết kế xoay quanh nó. Một cổng tiếp nhận được xây trên GetSheetNames hành xử giống hệt nhau trên một tệp 200 dòng và một tệp 200 MB, nên tệp chậm nhất trong một lô không còn quyết định nhịp độ khi cần xác định xem một tệp có đáng xử lý hay không
Một lệnh gọi cho cả .xls, .xlsx, và các định dạng template
Trên facade XLS, TXLSWorkbook.GetSheetNames đọc được nhiều hơn cả .xls. Nó cũng chấp nhận .xlsx, .xlsm, .xltx, và .xltm dựa trên zip, chỉ kéo workbook.xml ra khỏi archive. Với đầu vào .xls thực sự, nó quét các bản ghi BoundSheet rồi dừng lại ở bản ghi EOF đầu tiên của substream globals, nên một tệp nhị phân lớn vẫn chỉ tốn vài kilobyte mở đầu của nó. Facade XLSX mang theo một đảm bảo quan trọng hơn nhiều với code dịch vụ chạy dài hạn so với vẻ ngoài ban đầu của nó: TXLSXWorkbook.GetSheetNames không hề reset cũng không hề nạp dữ liệu vào thể hiện workbook, nên một thể hiện đang giữ sẵn một tài liệu đang mở vẫn có thể thăm dò các tệp khác mà không làm xáo trộn tệp đang cầm trong tay. GetODSSheetNames áp dụng cùng cách tiếp cận đó cho các package OpenDocument, và mỗi lệnh gọi trong số này đều có overload theo stream, cho phép bạn kiểm tra một tệp tải lên mà chưa từng chạm tới đĩa
var
Book: TXLSXWorkbook;
Names: TStringList;
I: Integer;
begin
Names := TStringList.Create;
Book := TXLSXWorkbook.Create;
try
if Book.GetSheetNames('upload-7f3a.xlsx', Names) <= 0 then
raise Exception.Create('unreadable workbook package');
if Names.IndexOf('Mapping') < 0 then
raise Exception.Create('required Mapping sheet is missing');
for I := 0 to Names.Count - 1 do
Writeln(Format('sheet %d: %s', [I, Names[I]]));
finally
Book.Free;
Names.Free;
end;
end;
Cùng lệnh gọi đó cũng tạo nên một hộp thoại import desktop tốt. Hãy liệt kê các sheet, để người dùng chọn một cái, và chỉ trả giá cho lần mở đầy đủ sau khi lựa chọn đã được thực hiện. Với một workbook năm mươi sheet, sự khác biệt hiện rõ: một picker xuất hiện ngay lập tức so với một picker khựng lại trong khi cả tệp đang tải phía sau nó
Các tệp .xlsm có bật macro và các định dạng template liệt kê y hệt một .xlsx thuần, vì mục lục nằm trong cùng một workbook.xml dù có hay không một vbaProject.bin đi kèm trong package. Vì vậy một pipeline tiếp nhận có thể liệt kê các sheet của một workbook có macro để định tuyến, mà không bao giờ đụng tới payload macro và không bao giờ làm bất cứ điều gì khiến nó chạy, rồi để quyết định chính sách macro cho giai đoạn thực sự mở tệp
Đọc giá trị trả về mà không tự lừa dối chính mình
Quy ước giá trị trả về không thống nhất trên toàn bộ HotXLS. Một số lệnh gọi trả về 1 khi thành công, số khác trả về một số đếm, nên với các hàm liệt kê, cách kiểm tra duy nhất đứng vững được là coi bất kỳ giá trị nào bằng 0 hoặc nhỏ hơn là thất bại, với danh sách chuỗi bị xóa sạch. Hãy cưỡng lại cám dỗ đọc một danh sách rỗng thành "một workbook không có sheet nào". Cả ECMA-376 lẫn đặc tả BIFF8 đều yêu cầu ít nhất một sheet trong một workbook hợp lệ, nên số tên bằng 0 luôn có nghĩa là việc đọc đã thất bại, chứ không bao giờ có nghĩa là tệp thực sự rỗng
Một lần liệt kê thất bại tự nó là một tín hiệu đáng giữ lại. Một tệp .xlsx khiến lệnh gọi thất bại thường rơi vào một trong vài trường hợp cụ thể: bị cắt cụt, thực ra không phải là một package OOXML nào cả (các tệp CSV xuất ra từ hệ thống khác bị gắn nhầm đuôi xuất hiện ở đây liên tục), hoặc là một container đã mã hóa. Phân biệt các trường hợp đó là việc của bước kiểm tra kế tiếp. Ghi log lại vài byte đầu của tệp bị từ chối cùng với thông báo thất bại thường biến một luồng hỗ trợ dài dòng thành một tin nhắn duy nhất
Phát hiện container đã mã hóa trước khi định tuyến
Một tệp .xlsx đã mã hóa không phải là một zip. Đó là một OLE compound file bọc quanh các luồng EncryptionInfo và EncryptedPackage, nên GetSheetNames không thể nhìn vào bên trong và trả về thất bại giống như bất kỳ tệp không đọc được nào khác. CanReadEncrypted kiểm tra đúng hình dạng container đó, cho phép khâu tiếp nhận cố ý định tuyến một tệp đã mã hóa thay vì nuốt chửng một lỗi đọc chung chung từ đâu đó sâu bên trong một worker:
type
TIntakeRoute = (irNormal, irNeedsPassword, irUnreadable);
function ClassifyUpload(const FileName: string; Names: TStrings): TIntakeRoute;
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
// OOXML đã mã hóa là một OLE container, không phải zip: kiểm tra trước,
// vì các lệnh liệt kê không thể nhìn vào bên trong nó.
if Book.CanReadEncrypted(FileName) then
Exit(irNeedsPassword);
if SameText(ExtractFileExt(FileName), '.ods') then
begin
if Book.GetODSSheetNames(FileName, Names) <= 0 then
Exit(irUnreadable);
end
else if Book.GetSheetNames(FileName, Names) <= 0 then
Exit(irUnreadable);
Result := irNormal;
finally
Book.Free;
end;
end;
Mã hóa là nơi HotXLS cố tình bất đối xứng, nên việc định tuyến phải tôn trọng điều đó. Mã hóa .xls kiểu cũ (RC4, RC4 CryptoAPI, XOR) là đọc được: TXLSWorkbook.Open(FileName, Password) giải mã bằng một mật khẩu đã lưu sẵn, và những tệp đó có thể ở lại trên tuyến đường tự động. Các package OOXML đã mã hóa lại đi theo hướng ngược lại. HotXLS có thể ghi ra một tệp như vậy bằng SaveAsEncrypted, nhưng nó không thể đọc lại tệp đó. OpenEncrypted ném ra EXlsxEncryptionNotImplemented khi được đưa cho một package đã mã hóa, đó là lý do vì sao một thiết kế tiếp nhận trung thực sẽ gửi .xlsx đã mã hóa cho một con người có Excel và giữ .xls mang mật khẩu lại trong code
Với công việc theo lô, bộ phân loại này xứng đáng có chỗ đứng của mình bằng cách chạy qua toàn bộ thư mục đầu vào trước khi bất kỳ worker nào bắt đầu xử lý thật sự, vì mỗi lần thăm dò chỉ tốn khoảng một lần mở tệp và vài kilobyte đọc dữ liệu. Đưa nó lên trước làm thay đổi kiểu thất bại mà bộ phận vận hành thực sự quan tâm. Thay vì một job chạy lúc 3 giờ sáng chết ngay ở tệp thứ 412 trên 600, bạn có được 412 tệp đã xếp hàng và 5 tệp bị từ chối ngay tại khâu tiếp nhận, kèm theo lý do cho từng tệp. Vẫn cùng những lệnh gọi thư viện đó, nhưng câu chuyện vận hành tốt hơn hẳn
Những câu hỏi mà một lệnh liệt kê không thể trả lời
Tên và thứ tự là toàn bộ những gì bạn nhận được. Các lệnh liệt kê không nói gì về tính hiển thị cả, nên các sheet ẩn và ẩn sâu xuất hiện trong danh sách giống hệt như mọi sheet khác. Chúng không báo cáo kích thước vùng đã dùng, không báo số ô, cũng không báo document property nào cả. Phần docProps/core.xml cũng nhỏ, nhưng hiện tại chưa có phép dò chỉ-riêng-property, nên metadata về tác giả và tiêu đề vẫn tốn một lần Open đầy đủ. Cách gọn gàng để sống chung với điều đó là để những sự thật rẻ tiền định tuyến mọi tệp, rồi dành những thứ đắt đỏ cho những tệp đã vượt qua bước định tuyến. Với những tệp thực sự tiến vào một lần đọc sâu, một lượt quét chỉ-đọc trên một tệp .xls lớn chạy nhanh hơn rõ rệt khi bật _DisableGraphics := True, giúp bỏ qua việc phân tích OfficeArt. Chỉ có điều đừng bao giờ lưu từ thể hiện đó: lớp drawing mà nó đã bỏ qua sẽ biến mất khỏi model, và việc lưu sẽ làm rơi mất nó khỏi tệp
Những tệp vượt qua bước sàng lọc thường tiến tới bước phân tích sâu hơn. workbench kiểm toán và chuyển đổi workbook trình bày các bộ đếm theo từng sheet đáng thu thập một khi một lần mở đầy đủ là hợp lý, còn hướng dẫn hiệu năng cho workbook lớn trình bày cách giữ cho lần mở đầy đủ đó vẫn nhanh
HotXLS là một thư viện bảng tính Object Pascal thuần cho Delphi và C++Builder; toàn bộ bề mặt API, bao gồm cả các lệnh kiểm tra được trình bày ở đây, đã được ghi lại trên trang sản phẩm HotXLS Delphi Component