Artikel Teknis

Pull Row Cursor untuk XLS, XLSX, ODS, dan CSV di Delphi

HotXLS membaca sumber .xls, .xlsx, .xlsm, .ods, CSV, dan TSV lewat satu pull row cursor, TXLSRowCursor, yang FindFirst dan FindNext-nya maju satu baris logis sekaligus sementara hanya baris itu yang tinggal di memori. State machine enam nilai memisahkan before-first dari EOF, cancelled, dan faulted, dan pembaca callback yang lebih tua kini menjadi adapter di atas cursor yang sama

Skenarionya akrab bagi siapa pun yang pernah mengirim fitur impor. Sebuah .xlsx 200 MB tiba, Anda memasang handler OnCell, dan persyaratan pertama setelah "bacalah" adalah "berhenti setelah seratus postingan terbalik pertama". Kini bentuk kode Anda melawan Anda: loop itu tinggal di dalam pustaka, handler Anda harus mengangkat sebuah flag, setiap callback berikutnya tetap menyala sampai parser menyadari, dan state yang terakumulasi — berapa kecocokan sejauh ini, kolom mana yang cocok, lalu apa selanjutnya — harus tinggal di field sebuah kelas yang ada hanya untuk memberi callback tempat duduk. Tak ada satu pun dari itu yang soal parsing. Itu soal kontrol aliran, dan itulah yang dihapus pull cursor

Apa yang sebenarnya dibayar callback push pada 200 MB

Push membalikkan kontrol, dan pembalikan itulah yang tak mampu dibeli pemanggil yang memfilter atau menggabungkan. Dengan API callback pustaka memiliki loop, jadi pemanggil tak bisa memakai Break, tak bisa menyisipkan dua sumber, tak bisa menyerahkan pembaca ke rutinitas yang mengharapkan digerakkan, dan tak bisa mengekspresikan "mengintip baris berikutnya sebelum memutuskan" tanpa buffering. Biayanya bukan throughput — jalur callback SAX yang ditulis baik mengalirkan dengan baik — melainkan bahwa setiap konsumen non-trivial menumbuhkan state machine kecil miliknya sendiri untuk mensimulasikan loop yang tak diizinkan ditulis. Kalikan itu dengan empat format file, masing-masing dulu punya titik masuk pemindaian sendiri, dan semantik filter, formula, serta error mulai menyimpang di antara mereka, persis hanyut yang hendak ditutup HotXLS

Bagaimana pull cursor mengubah kode pemanggil Anda?

Ia mengembalikan loop kepada Anda, beserta kontrol aliran Pascal biasa. TXLSRowCursor.Open menerima nama file atau TStream, mendeteksi format, memuat shared string dan metadata gaya-tanggal sekali, dan memilih sheet 1. SelectSheet (1-based) atau SelectSheetByName mengarahkan ulang ke worksheet lain dan me-reset cursor ke before-first. FindFirst dan FindNext kemudian memposisikan di baris terisi berikutnya — baris tanpa sel ter-decode dilewati, sehingga RowIndex bisa melompat — dan baris saat ini diekspos sebagai CellCount, Cells[], dan ValueByCol[], semuanya 1-based pada sumbu kolom. Meninggalkan loop adalah Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // skip the header band
    Cursor.IncludeColumn(1);     // decode only these two columns
    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;               // ordinary Break; no abort flag, no sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // the destructor ends the pass
  end;
end;

Proyeksi dan rentang diatur sebelum pass, bukan difilter setelahnya. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates, dan DetectTextTypes semuanya dihormati di dalam backend, sehingga kolom yang tak dipilih tak pernah mengalokasikan nilai, string formula, atau payload rich-text-nya sejak awal — regresi suite membuktikannya dengan formula 16 KiB dan string cache yang tak pernah dimaterialkan ketika kolomnya tak diproyeksikan. Opsi-opsi itu sengaja dibekukan selama pass aktif dan jadi bisa ditulis lagi di EOF, pada SelectSheet, atau setelah Close, sehingga satu pemindaian tak pernah bisa mencampur dua kontrak dekode. Jika Anda hanya butuh inventaris sheet alih-alih barisnya, pemuatan metadata-only dan sheet selektif adalah titik masuk yang lebih murah

Satu backend per format, satu loop pemindaian masing-masing

Setiap format punya tepat satu pemindai maju di dalam HotXLS, dan baik pull cursor maupun pembaca callback menggerakkan pemindai yang sama. TXLSXForwardRowBackend adalah satu-satunya state machine SAX worksheet untuk part sheet ECMA-376 Part 1 §18.3, memegang pembaca XML, tabel shared-formula, dan parser rich-text, serta maju tepat satu batas fisik <row> per panggilan. TXLSBiffForwardParser memiliki global, pemilihan sheet, dan maju baris untuk stream record [MS-XLS]; menjadikannya dapat dipause melahirkan kendala paling tajam di seluruh desain, karena string formula cache adalah record Formula yang langsung diikuti record String, sehingga titik penangguhan per-baris tak boleh pernah mendarat di antara keduanya. TXLSForwardTextBackend menyimpan pembaca sadar-BOM, delimiter aktif, dan satu record logis — CSV mengendus koma, titik koma, tab, atau pipa dari record pertama sambil mengabaikan karakter kutip, dan field kutip multi-baris disambung dengan #10 sehingga nomor baris mengikuti record logis alih-alih newline fisik. TXLSForwardOdsBackend menyimpan satu templat baris fisik untuk tabel OpenDocument §9, memperlakukan table:number-rows-repeated sebagai sisa-hitungan alih-alih penguraian, dan maju melewati sel tercakup tanpa menulis nilai. Pembaca langsung streaming berbagi loader shared-string dan gaya-tanggal yang sama

Pull row cursor HotXLS yang mendispatch ke satu pemindai maju per format, backend SAX untuk XLSX, parser record untuk BIFF, backend teks pengendus-delimiter, dan templat baris ODS, dengan pembaca callback dikonfigurasi di atasnya sebagai adapter
Setiap format punya tepat satu pemindai maju, dan baik pull cursor maupun pembaca callback menggerakkan pemindai itu, sehingga semantik filter dan error tak bisa saling menyimpang

Mengapa enam keadaan alih-alih satu flag Eof?

Karena satu boolean membuat empat situasi berbeda tak terbedakan, dan pemanggil menebak salah di semuanya. TXLSRowCursorState menamai mereka secara eksplisit

  • xrcsClosed — tak ada sumber yang terbuka
  • xrcsBeforeFirst — terbuka atau baru diarahkan ulang, belum ada baris dibaca
  • xrcsActive — berdiri di baris valid
  • xrcsEof — sheet dikonsumsi sampai habis
  • xrcsCancelled — pemanggil menghentikan pass dengan sengaja
  • xrcsFaulted — pass gagal dan exception asli sudah dilempar

Bedanya yang terakhir itulah yang penting di produksi. Part worksheet yang hilang atau awal pass yang gagal menahan EReadError-nya dan memindahkan cursor ke xrcsFaulted; ia tak pernah diturunkan menjadi False polos yang akan dibaca pemanggil sebagai "sheet ini kosong". Cancel sengaja lebih sempit dari Close: ia menutup backend worksheet saat ini beserta substream inflate-nya dan membatalkan baris saat ini, tetapi tak melepaskan arsip ZIP atau stream sumber, dan memanggilnya dua kali adalah no-op. Setelah cancel Anda melanjutkan dengan memanggil SelectSheet secara eksplisit — cursor tak akan diam-diam me-restart pass untuk Anda. Kepemilikan stream mengikuti aturan defensif yang sama: xsoBorrowed adalah bawaan dan memulihkan posisi stream saat tutup, xsoOwned mengalihkan kepemilikan hanya setelah Open sudah sukses, jadi open yang gagal tak pernah membebaskan stream yang masih dipegang pemanggil

Enam keadaan cursor baris HotXLS beserta transisi di antaranya, memperlihatkan Cancel memindahkan pass aktif ke cancelled, awal pass yang gagal memindahkannya ke faulted, dan bagaimana keduanya tetap berbeda dari akhir sheet
Enam keadaan bernama menjaga sheet kosong, penghentian sengaja, dan pass yang gagal tetap terbedakan, yang tak bisa dilakukan satu boolean Eof
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: the cursor never frees Src, and Close restores the
      // position the stream had when Open was called
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // closes the worksheet backend and its
            Break;           // inflate substream only; 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;                // still ours, still valid, position restored
  end;
end;

Meminjam baris saat ini tanpa menyalinnya

IXLSRowCursorView menyerahkan baris ke rutinitas lain tanpa menduplikasi array sel. View menyimpan guard bersama yang memegang pointer cursor plus counter generasi UInt64; maju, memilih sheet, cancel, close, dan menghancurkan cursor semuanya menaikkan generasi itu, dan penghancuran tambahan mengosongkan pemilik guard. Jadi view basi tak bisa membaca memori yang sudah bebas: Valid adalah probe tanpa-exception yang bisa dipanggil kapan pun, sementara setiap anggota lain memvalidasi lebih dulu dan melempar EXLSRowCursorViewInvalidated. Bersikap jujur tentang apa kontrak ini — itu fail-fast masa hidup, bukan jaminan thread-safety, dan ia tak memberi izin membaca sebuah baris dari thread kedua sementara thread pertama memajukan cursor

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // borrows; no cell array is copied
      for I := 0 to View.CellCount - 1 do
      begin
        Cell := View.Cells[I];
        if Cell.HasFormula and not Cell.FormulaTextAvailable then
          UseCachedResult(Cell.Value)     // BIFF forward reads keep the
        else if Cell.Kind = xdkEmpty then //   cached result, not the tokens
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank are real cells
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // The interface outlives the loop, but the row behind it does not
  if not View.Valid then    // Valid never raises; Cells[] now would raise
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, dan apa yang diizinkan dibuktikannya

PeakRowBufferedBytes ada untuk menunjukkan bahwa memori mengikuti lebar baris alih-alih jumlah baris. Ia mengakumulasi record sel, Variant, string formula, dan payload rich-text dari baris output saat ini, serta melipat set kerja spesifik-format — record logis CSV, templat baris fisik ODS, puncak record BIFF, atau sel mentah XLSX yang sedang di-decode. Bacalah bersama SheetPassesStarted, yang menghitung berapa pass worksheet yang benar-benar dimulai. Dua peringatan menjaga ini jujur: angkanya estimasi, bukan pembukuan heap eksak, dan ia monotonik sejak Open terakhir, jadi ia instrumen debug dan regresi alih-alih pengukur langsung. Untuk gambaran lebih luas ke mana waktu dan byte pergi pada buku yang sangat besar, lihat kinerja workbook besar di Delphi

Perbandingan HotXLS yang memperlihatkan muat seluruh-sheet menjaga setiap baris tetap residen versus pull cursor yang hanya memegang baris saat ini plus satu set kerja format, yang diakumulasi dan dilaporkan PeakRowBufferedBytes
PeakRowBufferedBytes mengakumulasi baris output saat ini plus set kerja spesifik-format, sehingga memori mengikuti seberapa lebar sebuah baris alih-alih berapa banyak baris yang dimiliki sheet

Pembaca push menjadi adapter, dan yang tak akan dilakukan cursor

TXLSForwardReader tak lagi membawa titik masuk pemindaian XLSX, BIFF, dan teks terpisah. Ia mengonfigurasi cursor, menyusurinya, dan menerjemahkan baris saat ini menjadi event OnSheet dan OnCell, dan itulah mengapa kedua fasad tak bisa lagi saling menyimpang pada filter, state formula, atau penanganan error. Dua konsekuensi layak diketahui sebelum Anda upgrade: callback SheetIndex kini seragam 1-based pada TXLSForwardReader (TXLSDirectReader mempertahankan kontrak event 0-based-nya yang ada), dan OnSheet menyala sebelum SelectSheet, sehingga mengatur SkipSheet berarti part worksheet tak pernah dibuka atau didekompresi sama sekali. Batas-batasnya sama eksplisitnya: workbook tidak boleh dimodifikasi selama pass aktif, cancel menuntut restart eksplisit, dan jalur maju BIFF tak pernah men-decompile token formula, sehingga sel formula klasik melaporkan HasFormula true dengan FormulaTextAvailable false dan menyerahkan hasil cache alih-alih mengarang string formula kosong. Row cursor dan adapter-nya lolos 1.298 pemeriksaan di Delphi Win32 dan Win64 plus paket statis C++Builder 37.0 Win64

Jika Anda sedang menimbang pull cursor versus loader yang Anda pegang sekarang, pertanyaannya bukan mana yang parse lebih cepat melainkan mana yang membiarkan Anda menulis kondisi keluar yang benar-benar Anda butuhkan. Detail komponen penuh, versi IDE yang didukung, dan lisensi ada di halaman komponen spreadsheet Delphi HotXLS