HotXLS đọc các nguồn .xls, .xlsx, .xlsm, .ods, CSV và TSV thông qua một pull row cursor duy nhất, TXLSRowCursor, mà FindFirst và FindNext của nó tiến từng logical row một trong khi chỉ row đó nằm trong bộ nhớ. Một state machine sáu giá trị tách before-first khỏi EOF, cancelled và faulted, và reader kiểu callback cũ giờ là một adapter bọc trên cùng cursor đó
Kịch bản này quen thuộc với bất kỳ ai từng tung một tính năng import. Một tệp .xlsx 200 MB tới, bạn nối một handler OnCell, và yêu cầu đầu tiên sau "đọc nó" là "dừng sau một trăm bút toán đảo đầu tiên". Lúc này cấu trúc code của bạn chống lại chính bạn: vòng lặp nằm trong thư viện, handler của bạn phải giơ cờ, mọi callback sau đó vẫn bắn cho tới khi parser để ý, và trạng thái tích lũy — đã trúng bao nhiêu, cột nào khớp, bước tiếp theo làm gì — phải nằm trong các field của một class tồn tại chỉ để cho callback có chỗ ngồi. Không thứ gì trong đó là bài toán parsing. Đó là bài toán control-flow, và đó chính là thứ pull cursor gỡ bỏ
Push callback thực trả giá bao nhiêu ở 200 MB
Push đảo ngược control, và sự đảo ngược đó đúng là thứ một caller lọc hay nối dữ liệu không thể gánh. Với API callback, thư viện sở hữu vòng lặp, nên caller không thể dùng Break, không thể đan hai nguồn vào nhau, không thể trao reader cho một routine mong được chủ động điều khiển, và không thể diễn đạt "nhìn trước row kế tiếp rồi mới quyết định" nếu không buffer. Cái giá không nằm ở throughput — một đường callback SAX viết tốt stream rất ổn — mà ở chỗ mọi consumer không tầm thường đều mọc thêm một state machine nhỏ của riêng nó để giả lập vòng lặp mà nó không được phép tự viết. Nhân điều đó với bốn định dạng tệp, mỗi cái vốn có entry point quét riêng, và ngữ nghĩa lọc, công thức lẫn lỗi bắt đầu trôi dần giữa chúng — chính là độ trôi mà HotXLS nhắm khép lại
Pull cursor đổi code gọi của bạn ra sao?
Nó trả vòng lặp lại cho bạn, kèm theo đó là control flow Pascal đời thường. TXLSRowCursor.Open nhận một tên tệp hoặc một TStream, nhận diện định dạng, nạp shared string và metadata kiểu ngày một lần, rồi chọn sheet 1. SelectSheet (1-based) hay SelectSheetByName nhắm lại worksheet khác và reset cursor về before-first. FindFirst và FindNext sau đó đặt vị trí trên row có dữ liệu kế tiếp — các row không có ô giải mã được bị bỏ qua, nên RowIndex có thể nhảy — và row hiện tại được phơi ra qua CellCount, Cells[] và ValueByCol[], tất cả đều 1-based trên trục cột. Rời vòng lặp chỉ là một Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // bỏ qua dải header
Cursor.IncludeColumn(1); // chỉ decode hai cột này
Cursor.IncludeColumn(7);
if not Cursor.Open('postings-200mb.xlsx') then
Exit;
if not Cursor.SelectSheetByName('Ledger') then
Exit;
Hits := 0;
if Cursor.FindFirst then
repeat
if VarToStr(Cursor.ValueByCol[7]) = 'REVERSED' then
begin
Inc(Hits);
if Hits = 100 then
Break; // Break đời thường; không cờ abort, không sentinel
end;
until not Cursor.FindNext;
finally
Cursor.Free; // destructor kết thúc pass
end;
end;
Phép chiếu và phạm vi được đặt trước pass, chứ không lọc sau. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates và DetectTextTypes đều được tôn trọng bên trong các backend, nên một cột không được chọn không bao giờ cấp phát giá trị, chuỗi công thức hay payload rich-text ngay từ đầu — bộ regression chứng minh điều đó với công thức 16 KiB và chuỗi cache không bao giờ được hiện thực hóa khi cột của chúng không được chiếu. Các tùy chọn này được cố ý đóng băng trong khi pass đang chạy và chỉ mở khóa ghi lại tại EOF, khi SelectSheet, hay sau Close, để một lượt quét không bao giờ trộn hai hợp đồng decode. Nếu bạn chỉ cần danh mục sheet chứ không cần các row, nạp chỉ metadata và chọn sheet riêng lẻ là cửa nhập rẻ hơn
Mỗi định dạng một backend, mỗi cái một vòng quét
Mỗi định dạng có đúng một forward scanner bên trong HotXLS, và cả pull cursor lẫn callback reader đều điều khiển chính scanner đó. TXLSXForwardRowBackend là state machine SAX duy nhất cho các sheet part ECMA-376 Part 1 §18.3, giữ XML reader, bảng shared-formula và parser rich-text, và nó tiến đúng một biên <row> vật lý mỗi lần gọi. TXLSBiffForwardParser sở hữu globals, chọn sheet và việc tiến row cho stream bản ghi [MS-XLS]; việc làm cho nó tạm dừng được tạo ra ràng buộc gắt nhất trong toàn bộ thiết kế, vì một công thức chuỗi cache là một bản ghi Formula theo ngay sau đó là bản ghi String, nên điểm treo từng row không bao giờ được đặt giữa hai bản ghi đó. TXLSForwardTextBackend giữ một reader biết BOM, delimiter đang dùng và một logical record — CSV dò phẩy, chấm phẩy, tab hay pipe từ bản ghi đầu tiên trong khi bỏ qua ký tự trong ngoặc kép, và các trường ngoặc kép nhiều dòng được nối bằng #10 để số hàng đuổi theo logical record thay vì ký tự xuống dòng vật lý. TXLSForwardOdsBackend giữ một template row vật lý duy nhất cho các bảng OpenDocument §9, coi table:number-rows-repeated là một số đếm còn lại chứ không phải một phép mở rộng, và tiến qua các ô được phủ mà không phát giá trị. Streaming direct reader dùng chung bộ nạp shared-string và date-style đó
Vì sao sáu trạng thái thay vì một cờ Eof?
Vì một boolean duy nhất khiến bốn tình huống khác nhau không thể phân biệt, và caller đoán sai cả bốn. TXLSRowCursorState gọi tên chúng một cách tường minh
xrcsClosed— chưa mở nguồn nàoxrcsBeforeFirst— đã mở hay đã nhắm lại đích, chưa đọc row nàoxrcsActive— đang đứng trên một row hợp lệxrcsEof— sheet đã bị tiêu thụ đến tận cùngxrcsCancelled— caller chủ động dừng passxrcsFaulted— pass thất bại và ngoại lệ gốc đã được raise
Phân biệt cuối cùng là thứ có ý nghĩa trong production. Một sheet part thiếu hay một lần khởi động pass thất bại giữ nguyên EReadError của nó và đưa cursor về xrcsFaulted; nó không bao giờ bị hạ cấp thành một False trơn mà caller sẽ đọc là "sheet này rỗng". Cancel được cố ý thu hẹp hơn Close: nó đóng backend worksheet hiện tại cùng inflate substream của nó và vô hiệu row hiện tại, nhưng không giải phóng kho ZIP hay stream nguồn, và gọi nó hai lần là no-op. Sau một cancel, bạn tiếp tục bằng cách gọi SelectSheet tường minh — cursor sẽ không lặng lẽ khởi động lại pass thay bạn. Quyền sở hữu stream đi theo cùng quy tắc phòng thủ: xsoBorrowed là mặc định và khôi phục vị trí stream khi đóng, xsoOwned chỉ chuyển quyền sở hữu sau khi Open đã thành công, nên một lần mở thất bại không bao giờ giải phóng một stream mà caller vẫn đang giữ
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: cursor không bao giờ giải phóng Src, và Close khôi phục
// vị trí stream có được khi Open được gọi
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // chỉ đóng backend worksheet và
Break; // inflate substream của nó; idempotent
end;
until not Cursor.FindNext;
case Cursor.State of
xrcsEof: Log('sheet consumed to the end');
xrcsCancelled: Log('stopped by the operator');
xrcsFaulted: Log('pass failed; the EReadError was already raised');
end;
finally
Cursor.Free;
end;
finally
Src.Free; // vẫn của ta, vẫn hợp lệ, vị trí đã khôi phục
end;
end;
Mượn row hiện tại mà không sao chép nó
IXLSRowCursorView trao một row cho routine khác mà không nhân bản mảng ô. View giữ một guard dùng chung chứa con trỏ cursor cộng một bộ đếm generation UInt64; việc tiến, chọn sheet, cancel, đóng và hủy cursor đều tăng generation đó, và việc hủy còn xóa cả chủ guard. Nhờ vậy một view cũ kỹ không thể đọc bộ nhớ đã giải phóng: Valid là phép dò không bao giờ văng ngoại lệ mà bạn có thể gọi bất cứ lúc nào, còn mọi thành viên khác kiểm tra trước rồi raise EXLSRowCursorViewInvalidated. Hãy trung thực về bản chất hợp đồng này — đó là vòng đời fail-fast, không phải cam kết thread-safety, và nó không cấp phép đọc một row từ thread thứ hai trong khi thread đầu đang tiến cursor
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // mượn; không mảng ô nào được sao chép
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // forward read BIFF giữ
else if Cell.Kind = xdkEmpty then // kết quả cache, không phải token
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank là ô thật
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// Interface sống lâu hơn vòng lặp, nhưng row đằng sau thì không
if not View.Valid then // Valid không bao giờ raise; Cells[] lúc này sẽ raise
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes, và những gì nó được phép chứng minh
PeakRowBufferedBytes tồn tại để chứng minh bộ nhớ đi theo độ rộng hàng chứ không phải số hàng. Nó cộng dồn các bản ghi ô, Variant, chuỗi công thức và payload rich-text của row đầu ra hiện tại, rồi gộp thêm working set riêng của định dạng — logical record của CSV, template row vật lý của ODS, đỉnh bản ghi của BIFF, hay ô thô XLSX đang được decode. Hãy đọc nó cùng SheetPassesStarted, thứ đếm bao nhiêu pass worksheet thực sự bắt đầu. Hai lưu ý giữ cho chỉ số này trung thực: con số là một ước lượng, không phải kế toán heap chính xác, và nó đơn điệu tăng kể từ Open gần nhất, nên nó là công cụ debug và regression chứ không phải đồng hồ đo trực tiếp. Để có bức tranh rộng hơn về thời gian và bộ nhớ đi đâu trên những cuốn sách rất lớn, xem hiệu năng workbook lớn trong Delphi
Push reader hóa thành adapter, và những gì cursor không làm
TXLSForwardReader không còn mang các entry point quét riêng cho XLSX, BIFF và văn bản. Nó cấu hình một cursor, đi hết cursor đó, và dịch row hiện tại thành các sự kiện OnSheet và OnCell, đó là lý do hai mặt ngoài không thể trôi tách nhau về lọc, trạng thái công thức hay xử lỗi nữa. Hai hệ quả đáng biết trước khi nâng cấp: SheetIndex của callback giờ nhất quán 1-based trên TXLSForwardReader (TXLSDirectReader giữ hợp đồng sự kiện 0-based hiện có của nó), và OnSheet bắn trước SelectSheet, nên đặt SkipSheet nghĩa là phần worksheet không bao giờ được mở hay giải nén. Ranh giới cũng tường minh không kém: workbook không được sửa trong khi pass đang chạy, cancel đòi hỏi khởi động lại tường minh, và đường forward BIFF không bao giờ decompile token công thức, nên ô công thức classic báo HasFormula true với FormulaTextAvailable false và trao cho bạn kết quả cache thay vì bịa một chuỗi công thức rỗng. Row cursor cùng adapter của nó đã qua 1.298 phép kiểm tra trên Delphi Win32 và Win64 cộng gói tĩnh C++Builder 37.0 Win64
Nếu bạn đang cân một pull cursor với loader đang có, câu hỏi cần hỏi không phải cái nào parse nhanh hơn mà là cái nào cho phép bạn viết điều kiện thoát mà bạn thực sự cần. Chi tiết component đầy đủ, các phiên bản IDE được hỗ trợ và licensing nằm trên trang HotXLS Delphi spreadsheet component