Odborný článok

Pull kurzor riadkov pre XLS, XLSX, ODS a CSV v Delphi

HotXLS číta zdroje .xls, .xlsx, .xlsm, .ods, CSV a TSV cez jediný pull kurzor riadkov, TXLSRowCursor, ktorého FindFirst a FindNext postupujú po jednej logickejj riadku, zatiaľ čo v pamäti ostáva len ten riadok. Šesťhodnotový stavový automat oddeľuje before-first od EOF, cancelled a faulted a starší callback čítačka je teraz adaptér nad tým istým kurzorom

Scenár je známy každému, kto odoslal importovaciu vlastnosť. 200 MB .xlsx príde, napojíte handler OnCell a prvou požiadavkou po „prečítaj to" je „zastav po prvých stovke reversovaných účtovných zápisov". Teraz tvar vášho kódu proti vám bojuje: slučka žije vo vnútri knižnice, váš handler musí zdvihnúť vlajku, každý následný callback sa stále spúšťa, kým parser si toho všimne, a akumulovaný stav — koľko zásahov doteraz, ktorý stĺpec zodpovedal, čo robiť ďalej — musí žiť v poliach triedy, ktorá existuje len preto, aby dala callbacku miesto, kde sedieť. Nič na tom nie je problém parsovania. Je to problém toku riadenia a je to ten, ktorý pull kurzor odstráni

Čo push callback skutočne stojí pri 200 MB

Push prevracia riadenie a prevrátenie je presne to, čo si volajúci filtrujúci alebo spájajúci nemôže dovoliť. S callback API vlastní knižnica slučku, takže volajúci nemôže použiť Break, nemôže prelínasť dva zdroje, nemôže odovzdať čítačku rutine, ktorá očakáva, že bude riadená, a nemôže vyjadriť „pozri na nasledujúci riadok pred rozhodnutím" bez bufferovania. Náklady nie sú throughput — dobre napísaná SAX callback cesta streamuje v poriadku — je to, že každý netriviálny konzument si vypestuje vlastný malý stavový automat na simuláciu slučky, ktorú mu nedovolili napísať. Vynásobte to štyrmi formátmi súborov, každý historicky s vlastným vstupným bodom skenovania, a sémantika filtrovania, vzorcov a chýb začne driftovať medzi nimi, čo je presne ten drift, ktorý HotXLS zamýšľal zavrieť

Ako pull kurzor mení váš volací kód?

Vec vráti slučku vám a s ňou obyčajný Pascal tok riadenia. TXLSRowCursor.Open prijme názov súboru alebo TStream, deteguje formát, raz načíta zdieľané reťazce a metadata dátových štýlov a vyberie list 1. SelectSheet (1-based) alebo SelectSheetByName precieli na iný pracovný list a resetujú kurzor na before-first. FindFirst a FindNext potom pozícia na ďalšom obsadenom riadku — riadky bez dekódovateľných buniek sa preskočia, takže RowIndex môže skákať — a aktuálny riadok je vystavený ako CellCount, Cells[] a ValueByCol[], všetko 1-based na osi stĺpcov. Opustenie slučky je Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // preskoč hlavičkový pás
    Cursor.IncludeColumn(1);     // dekóduj len tieto dva stĺpce
    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;               // obyčajný Break; žiadna abort vlajka, žiadny sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // deštruktor ukončí passu
  end;
end;

Projekcia a rozsah sa nastavujú pred passu, nie filtrujú potom. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates a DetectTextTypes sa všetky rešpektujú vo vnútri backendov, takže nevybraný stĺpec nikdy nealokuje svoju hodnotu, reťazec vzorca ani rich-text payload — regresná sada to dokazuje s 16 KiB vzorcami a uloženými reťazcami, ktoré sa nikdy nematerializujú, keď ich stĺpec nie je projekovaný. Tie voľby sú zámerne zamrznuté, kým je passa aktívna, a stanú sa opäť zapisovateľnými pri EOF, na SelectSheet alebo po Close, takže jediný sken nikdy nemôže zmiešať dva dekódovacie kontrakty. Ak potrebujete len inventár listov namiesto riadkov, načítanie len metadát a selektívne načítanie listov je lacnejší vstupný bod

Jeden backend na formát, jedna skenovacia slučka každý

Každý formát má presne jednu doprednú skenovačku vo vnútri HotXLS a pull kurzor aj callback čítačka riadia tú istú skenovačku. TXLSXForwardRowBackend je jediný worksheet SAX stavový automat pre sheet časti ECMA-376 Part 1 §18.3, drží XML čítačku, tabuľku zdieľaných vzorcov a rich-text parser a postupuje na presne jednu fyzickú hranicu <row> na volanie. TXLSBiffForwardParser vlastní globály, výber listu a postup riadku pre record stream [MS-XLS]; urobiť ho pauzovateľným produkovalo najostrejšie obmedzenie celého návrhu, pretože uložený reťazcový vzorec je záznam Formula bezprostredne nasledovaný záznamom String, takže bod per-riadkovej suspenzie sa nikdy nesmie pristáť medzi tými dvoma. TXLSForwardTextBackend drží BOM-vedomú čítačku, aktívny delimiter a jeden logický záznam — CSV oňuchá čiarku, bodkočiarku, tabulátor alebo pipe z prvého záznamu pri ignorovaní znakov v úvodzovkách a viacriadkové úvodzkované polia sa spájajú s #10, takže číslo riadku sleduje logické záznamy namiesto fyzických nových riadkov. TXLSForwardOdsBackend drží jedinú fyzickú riadkovú šablónu pre tabuľky OpenDocument §9, považuje table:number-rows-repeated za zostávajúci počet namiesto expanzie a postupuje cez pokryté bunky bez emitovania hodnôt. Streamovanie direct reader zdieľa rovnaký loader zdieľaných reťazcov a dátových štýlov

Pull kurzor riadkov HotXLS dispatchujúci na jednu doprednú skenovačku na formát, SAX backend pre XLSX, record parser pre BIFF, textový backend oňuchávajúci delimiter a ODS riadková šablóna, s callback čítačkou nakonfigurovanou navrchu ako adaptér
Každý formát má presne jednu doprednú skenovačku a pull kurzor aj callback čítačka riadia tú istú skenovačku, takže sémantika filtrovania a chýb sa nemôže rozísť

Prečo šesť stavov namiesto jednej vlajky Eof?

Pretože jediný boolean robí štyri rôzne situácie neodlíšiteľnými a volajúci hádajú zle o všetkých. TXLSRowCursorState ich menuje explicitne

  • xrcsClosed — žiadny zdroj nie je otvorený
  • xrcsBeforeFirst — otvorený alebo precielený, žiadny riadok ešte neprečítaný
  • xrcsActive — stojí na platnom riadku
  • xrcsEof — list bol skonzumovaný do konca
  • xrcsCancelled — volajúci zastavil passu zámerne
  • xrcsFaulted — passa zlyhala a pôvodná výnimka bola vyvolaná

Toto posledné rozlíšenie je to, ktoré záleží v produkcii. Chýbajúca sheet časť alebo zlyhaný štart passy drží svoje EReadError a presunie kurzor na xrcsFaulted; nikdy sa nedegraduje na obyčajné False, ktoré by volajúci čítal ako „tento list bol prázdny". Cancel je zámerne užšie než Close: zatvorí backend aktuálneho pracovného listu a jeho inflate substream a zneplatní aktuálny riadok, ale neuvolní ZIP archív ani zdrojový stream a dvojité zavolanie je no-op. Po canceli obnovíte explicitným zavolaním SelectSheet — kurzor nebude mlčky reštartovať passu za vás. Vlastníctvo streamu nasleduje rovnaké obranné pravidlo: xsoBorrowed je predvolené a obnovuje pozíciu streamu pri zatvorení, xsoOwned prevedie vlastníctvo až po tom, čo Open už uspel, takže zlyhané open nikdy neuvolní stream, ktorý volajúci ešte drží

Šesť stavov kurzoru riadkov HotXLS s prechodmi medzi nimi, ukazujúce Cancel presúvajúci aktívnu passu na cancelled, zlyhaný štart passy presúvajúci ju na faulted a ako oba ostávajú odlišné od konca listu
Šesť pomenovaných stavov drží prázdny list, zámerne zastavenie a zlyhanú passu odlíšiteľné, čo jediná boolean vlajka Eof nedokáže
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: kurzor nikdy neuvolní Src a Close obnoví
      // pozíciu, ktorú stream mal, keď bolo volané Open
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // zatvára len backend pracovného listu a jeho
            Break;           // inflate substream; idempotentné
          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;                // stále náš, stále platný, pozícia obnovená
  end;
end;

Požičanie aktuálneho riadku bez kopírovania

IXLSRowCursorView odovzdá riadok inej rutine bez duplikovania poľa buniek. Pohľad drží zdieľanú stráž držiacu ukazovateľ kurzora plus UInt64 počítadlo generácií; postupovanie, výber listu, cancel, zatvorenie a zničenie kurzora všetky inkrementujú tú generáciu a zničenie navyše vymaže vlastníka stráže. Takže zastaraný pohľad nemôže čítať uvoľnenú pamäť: Valid je bezvýnimkový prieskum, ktorý môžete zavolať kedykoľvek, zatiaľ čo každý iný člen najprv validuje a vyvolá EXLSRowCursorViewInvalidated. Buďte úprimní v tom, čo tento kontrakt je — je to lifetime fail-fast, nie záruka thread-safety a nelicencuje čítanie riadku z druhého vlákna, kým prvé posúva kurzor

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // požičiava; žiadne pole buniek sa nekopíruje
      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 dopredné čítania držia
        else if Cell.Kind = xdkEmpty then //   uložený výsledok, nie tokeny
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank sú skutočné bunky
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Rozhranie prežije slučku, ale riadok za ním nie
  if not View.Valid then    // Valid nikdy nevyvolá; Cells[] teraz by vyvolalo
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes a čo smie dokázať

PeakRowBufferedBytes existuje na demonštráciu, že pamäť sleduje šírku riadku, nie počet riadkov. Akumuluje záznamy buniek, Variants, reťazce vzorcov a rich-text payloady aktuálneho výstupného riadku a skladá dovnútra formátovo-špecifický pracovný set — CSV logický záznam, ODS fyzickú riadkovú šablónu, BIFF record peak alebo XLSX surovú bunku práve dekódovanú. Čítajte ho spolu s SheetPassesStarted, ktoré počíta, koľko passí pracovného listu skutočne začalo. Dve výhrady držia to úprimné: číslo je odhad, nie presné heap účtovníctvo, a je monotonické od najnovšieho Open, takže je to ladiaci a regresný inštrument, nie živý merač. Pre širší obraz, kam idú čas a bajty na veľmi veľkých zošitoch, pozrite výkon veľkých zošitov v Delphi

Porovnanie HotXLS ukazujúce načítanie celého listu držiaceho každý riadok rezidentný oproti pull kurzoru držiacemu len aktuálny riadok plus jeden formátový pracovný set, čo je to, čo PeakRowBufferedBytes akumuluje a hlási
PeakRowBufferedBytes akumuluje aktuálny výstupný riadok plus formátovo-špecifický pracovný set, takže pamäť sleduje, aký široký je riadok, nie koľko riadkov list má

Push čítačka sa stala adaptérom a čo kurzor neurobí

TXLSForwardReader už nenesie samostatné vstupné body skenovania XLSX, BIFF a textu. Konfiguruje kurzor, prechádza ho a prekladá aktuálny riadok na udalosti OnSheet a OnCell, čo je dôvod, prečo si tie dve fasády už nemôžu rozísť na filtrovaní, formulovom stave ani spracovaní chýb. Dva dôsledky stoja za poznanie pred upgradom: callback SheetIndex je teraz jednotne 1-based na TXLSForwardReader (TXLSDirectReader drží svoj existujúci 0-based event kontrakt) a OnSheet sa spúšťa pred SelectSheet, takže nastavenie SkipSheet znamená, že sheet časť pracovného listu sa nikdy neotvorí ani nedekomprimuje vôbec. Hranice sú rovnako explicitné: zošit nesmie byť modifikovaný, kým je passa aktívna, cancelovanie vyžaduje explicitný reštart a BIFF dopredná cesta nikdy nedešifruje tokeny vzorcov, takže klasické formulové bunky hlásia HasFormula true s FormulaTextAvailable false a podajú vám uložený výsledok namiesto vymýšľania prázdneho reťazca vzorca. Kurzor riadkov a jeho adaptér prešli 1 298 kontrolami na Delphi Win32 a Win64 plus C++Builder 37.0 Win64 statický balík

Ak vážite pull kurzor proti loaderu, ktorý máte teraz, otázka, ktorú si treba položiť, nie je, ktorý parsuje rýchlejšie, ale ktorý vám dovolí napísať výstupnú podmienku, ktorú skutočne potrebujete. Kompletné detaily komponentu, podporované verzie IDE a licencovanie sú na stránke HotXLS Delphi spreadsheet component