Technický článek

Tahací kurzor řádků pro XLS, XLSX, ODS a CSV v Delphi

HotXLS čte zdroje .xls, .xlsx, .xlsm, .ods, CSV a TSV jedním tahacím kurzorem řádků TXLSRowCursor, jehož FindFirst a FindNext posouvají po jedné logické řádce, přičemž v paměti zůstává jen tento řádek. Šestihodnotový stavový automat rozlišuje stav před prvním řádkem od konce souboru, zrušení a chyby a starší čtečka s callbacky je dnes adaptérem nad stejným kurzorem

Scénář zná každý, kdo někdy dodával funkci importu. Přijde .xlsx o 200 MB, napojíte handler OnCell a prvním požadavkem po „přečti to" je „zastav se po prvních sta obrácených účtování". Tvář vašeho kódu vám teď brání: smyčka žije uvnitř knihovny, handler musí nastavit příznak, všechny další callbacky se stále spouštějí, dokud to parser nepozná, a nashromážděný stav — kolik zásahů doposud, který sloupec seděl, co dál — musí žít v polích třídy, která existuje jen proto, aby měla callback kde sedět. Na ničem z toho není problém parsování. Je to problém řízení toku a právě ten tahací kurzor odstraňuje

Co ve skutečnosti stojí push callback při 200 MB

Push obrací řízení a právě inverzi si volající, který filtruje nebo slučuje, nemůže dovolit. S callback API vlastní smyčku knihovna, takže volající nemůže použít Break, nemůže prokládat dva zdroje, nemůže předat čtečku rutině, která očekává, že ji bude někdo řídit, a nemůže bez bufferování vyjádřit „nejprve se podívej na další řádek a teprve pak se rozhodni". Cena není v propustnosti — dobře napsaná SAX cesta streamuje v pořádku — jde o to, že každý netriviální konzument si vypěstuje vlastní malý stavový automat, aby nasimuloval smyčku, kterou mu napsat nebylo dovoleno. Vynásobte to čtyřmi formáty souborů, z nichž každý má historicky vlastní vstupní bod skenování, a sémantika filtrování, vzorců a chyb začne mezi nimi rozcházet — právě ten rozchod chtělo HotXLS zavřít

Jak tahací kurzor mění váš volající kód?

Vrací vám smyčku, a s ní obyčejný řídicí tok Pascalu. TXLSRowCursor.Open přijme název souboru nebo TStream, rozpozná formát, jednou načte sdílené řetězce a metadata stylů data a vybere list 1. SelectSheet (indexace od 1) nebo SelectSheetByName přesměruje na jiný list a nastaví kurzor zpět před první řádek. FindFirst a FindNext pak stanou na další obsazené řádce — řádky bez dekódovatelných buněk se přeskočí, takže RowIndex může skákat — a aktuální řádek je vystaven jako CellCount, Cells[] a ValueByCol[], vše s indexací sloupců od 1. Opuštění smyčky je Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // přeskoč záhlaví
    Cursor.IncludeColumn(1);     // dekóduj jen tyto dva sloupce
    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čejné Break; žádný příznak přerušení, žádný sentinel
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktor průchod ukončí
  end;
end;

Projekce a rozsah se nastavují před průchodem, nikoli filtrují potom. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates a DetectTextTypes respektují backendy uvnitř, takže nevybraný sloupec nikdy nealokuje svou hodnotu, řetězec vzorce ani rich-text data — regresní sada to dokazuje vzorci 16 KiB a cachovanými řetězci, které se nikdy nevytvoří, když jejich sloupec není projektován. Tyto volby jsou za aktivního průchodu záměrně zamrzlé a znovu se stanou zapisovatelnými na konci souboru, při SelectSheet nebo po Close, takže jeden sken nikdy nemíchá dvě dekódovací smlouvy. Pokud potřebujete jen inventář listů a ne řádky, levnějším vstupním bodem je načítání jen metadat a vybraných listů

Jeden backend na formát, jedna skenovací smyčka

Každý formát má uvnitř HotXLS přesně jeden dopředný skener a tahací kurzor i čtečka s callbacky řídí tentýž skener. TXLSXForwardRowBackend je jediný SAX stavový automat listu pro části listů ECMA-376 Part 1 §18.3, drží čtečku XML, tabulku sdílených vzorců a parser rich-textu a posune se na přesně jednu fyzickou hranici <row> na volání. TXLSBiffForwardParser vlastní globály, výběr listu a posun řádků pro proud záznamů [MS-XLS]; umožnit mu pauzu vyneslo nejostrější omezení celého návrhu, protože cachovaný řetězcový vzorec je záznam Formula bezprostředně následovaný záznamem String, takže bod pozastavení na řádek nesmí nikdy spadnout mezi ně. TXLSForwardTextBackend drží čtečku známou s BOM, aktivní oddělovač a jednu logickou položku — CSV z první položky vyčuchá čárku, středník, tabulátor nebo svislítko a ignoruje znaky v uvozovkách, a víceřádkové pole v uvozovkách se spojí s #10, takže číslo řádku sleduje logické položky, nikoli fyzické konce řádků. TXLSForwardOdsBackend drží jednu fyzickou šablonu řádku pro tabulky OpenDocument §9, bere table:number-rows-repeated jako zbývající počet, nikoli expanzi, a posouvá se přes pokryté buňky bez vydávání hodnot. Streamovací přímá čtečka sdílí stejný zavaděč sdílených řetězců a stylů data

Tahací kurzor řádků HotXLS rozesílá práci po jednom dopředném skeneru na formát, SAX backend pro XLSX, parser záznamů pro BIFF, textový backend vyčuchávající oddělovač a šablonu řádků ODS, s čtečkou callbacků nastavenou nahoře jako adaptér
Každý formát má přesně jeden dopředný skener a oba, tahací kurzor i čtečka callbacků, řídí tentýž skener, takže sémantika filtrování a chyb se nemůže rozcházet

Proč šest stavů místo jednoho příznaku Eof?

Protože jediná boolean hodnota učiní čtyři různé situace nerozlišitelnými a volající se u všech mýlí. TXLSRowCursorState je pojmenuje explicitně

  • xrcsClosed — žádný zdroj není otevřen
  • xrcsBeforeFirst — otevřeno nebo přesměrováno, žádný řádek ještě načten
  • xrcsActive — stojí na platném řádku
  • xrcsEof — list byl spotřebován až do konce
  • xrcsCancelled — volající průchod záměrně zastavil
  • xrcsFaulted — průchod selhal a původní výjimka byla vyvolána

Právě to poslední rozlišení hraje roli v produkci. Chybějící část listu nebo nezdařený start průchodu si podrží své EReadError a přesune kurzor do xrcsFaulted; nikdy se nesníží na holé False, které by volající četl jako „tento list byl prázdný". Cancel je záměrně užší než Close: zavře backend aktuálního listu a jeho podproud inflace a zneplatní aktuální řádek, ale neuvolní ZIP archiv ani zdrojový proud a dvojí volání je no-op. Po zrušení pokračujete explicitním voláním SelectSheet — kurzor za vás tiše průchod nerestartuje. Vlastnictví proudu následuje totéž obranné pravidlo: xsoBorrowed je výchozí a při zavření obnoví pozici proudu, xsoOwned přenese vlastnictví jen poté, co Open již uspělo, takže neúspěšné otevření nikdy neuvolní proud, který volající stále drží

Šest stavů kurzoru řádků HotXLS s přechody mezi nimi: Cancel převádí aktivní průchod do zrušeno, nezdařený start průchodu do selhalo a obojí zůstává odlišné od konce listu
Šest pojmenovaných stavů udržuje rozlišitelné prázdný list, záměrné zastavení a nezdařený průchod, což jediná boolean hodnota Eof nedokáže
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: kurzor Src nikdy neuvolní a Close obnoví
      // pozici, kterou proud měl při volání Open
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // zavře jen backend listu a jeho
            Break;           // podproud inflace; 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ý, pozice obnovena
  end;
end;

Vypůjčení aktuálního řádku bez kopírování

IXLSRowCursorView předá řádek jiné rutině bez duplikování pole buněk. Pohled ukládá sdílenou stráž držící ukazatel kurzoru plus UInt64 počítadlo generace; posun, výběr listu, zrušení, zavření i zničení kurzoru generaci zvyšují a zničení navíc vymaže vlastníka stráže. Zastaralý pohled tedy nemůže číst uvolněnou paměť: Valid je sond bez výjimek, kterou lze volat kdykoli, zatímco každý ostatní člen se nejprve validuje a vyvolá EXLSRowCursorViewInvalidated. Buďte upřímní co je tato smlouva — je to fail-fast životnosti, ne záruka bezpečnosti vláken, a nelicencuje čtení řádku z druhého vlákna, zatímco první posouvá kurzor

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // půjčí; pole buněk se 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)     // dopředné čtení BIFF podrží
        else if Cell.Kind = xdkEmpty then //   cachovaný výsledek, ne tokeny
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank jsou skutečné buňky
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Rozhraní přežije smyčku, ale řádek za ní ne
  if not View.Valid then    // Valid nikdy nevyvolá; Cells[] teď by vyvolala
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes a co smí prokázat

PeakRowBufferedBytes existuje k tomu, aby demonstroval, že paměť sleduje šířku řádku, nikoli počet řádků. Akumuluje záznamy buněk, Variants, řetězce vzorců a rich-text data aktuálního výstupního řádku a zahrne formátově specifickou pracovní sadu — CSV logickou položku, šablonu fyzického řádku ODS, špičku záznamů BIFF nebo surovou buňku XLSX právě dekódovanou. Čtěte jej spolu s SheetPassesStarted, které počítá, kolik průchodů listem skutečně začalo. Dvě varování drží údaj poctivým: číslo je odhad, ne přesné účtování haldy, a je monotónní od posledního Open, takže jde o ladicí a regresní nástroj, nikoli živý měřič. Širší obraz o tom, kam jdou čas a bajty u velmi velkých sešitů, přináší článek výkonnost velkých sešitů v Delphi

Srovnání HotXLS ukazuje načtení celého listu držící rezidentní každý řádek proti tahacímu kurzoru držícímu jen aktuální řádek plus jednu formátovou pracovní sadu, což je to, co PeakRowBufferedBytes akumuluje a hlásí
PeakRowBufferedBytes akumuluje aktuální výstupní řádek plus formátově specifickou pracovní sadu, takže paměť sleduje, jak je řádek široký, nikoli kolik list řádků má

Push čtečka se stala adaptérem a co kurzor neudělá

TXLSForwardReader už nenese samostatné vstupní body skenování pro XLSX, BIFF a text. Nakonfiguruje kurzor, projde jej a přeloží aktuální řádek do událostí OnSheet a OnCell, takže obě fasády se už nemohou rozcházet v filtrování, stavu vzorců ani zpracování chyb. Dva důsledky stojí za znalost před upgradem: callback SheetIndex je nyní na TXLSForwardReader jednotně indexován od 1 (TXLSDirectReader si podrží svou stávající smlouvu událostí od 0) a OnSheet se spustí před SelectSheet, takže nastavení SkipSheet znamená, že část listu se vůbec neotevře ani nerozbalí. Hranice jsou stejně explicitní: sešit nesmí být za aktivního průchodu upraven, zrušení vyžaduje explicitní restart a dopředná cesta BIFF nikdy nedekompiluje tokeny vzorců, takže klasické buňky se vzorcem hlásí HasFormula true s FormulaTextAvailable false a podávají cachovaný výsledek, místo aby vymyslely prázdný řetězec vzorce. Kurzor řádků a jeho adaptér prošly 1 298 kontrolami na Delphi Win32 a Win64 plus statickém balíčku C++Builder 37.0 Win64

Pokud vážíte tahací kurzor proti zavaděči, který máte nyní, otázka není, který parsuje rychleji, ale který vám dovolí napsat výstupní podmínku, kterou skutečně potřebujete. Úplné detaily komponenty, podporované verze IDE a licencování jsou na stránce HotXLS Delphi spreadsheet component