Tehnički članak

Pull Row Cursor za XLS, XLSX, ODS i CSV u Delphiju

HotXLS čita .xls, .xlsx, .xlsm, .ods, CSV i TSV izvore kroz jedinstveni pull row cursor, TXLSRowCursor, čiji FindFirst i FindNext napreduju po jedan logički redak dok u memoriji ostaje samo taj redak. Automat sa šest stanja razdvaja prije-prvog retka od EOF, otkazanog i neuspjelog, a stariji čitač s povratnim pozivom sada je adapter nad istim kurzorom

Scenarij je poznat svakome tko je isporučio značajku uvoza. Stigne 200 MB .xlsx, uključite rukovatelj OnCell, a prvi zahtjev nakon „pročitaj to" jest „zaustavi se nakon prvih stotinu obrnutih knjiženja". Sad vam oblik koda pruža otpor: petlja živi unutar biblioteke, vaš rukovatelj mora podići zastavicu, svaki kasniji callback i dalje se aktivira dok parser ne primijeti, a nakupljeno stanje — koliko pogodaka do sada, koji je stupac pogodio, što dalje — mora živjeti u poljima klase koja postoji samo da bi callbacku dala gdje sjesti. Ništa od toga nije problem parsiranja. To je problem toka upravljanja, i to je upravo ono što pull cursor uklanja

Što push callback stvarno košta pri 200 MB

Push okreće upravljanje, a inverzija je upravo ono što pozivatelj koji filtrira ili spaja ne može priuštiti. S callback API-jem biblioteka posjeduje petlju, pa pozivatelj ne može koristiti Break, ne može isprepliti dva izvora, ne može predati čitač rutini koja očekuje da bude vođena i ne može izraziti „pogledaj sljedeći redak prije odluke" bez međuspremanja. Cijena nije propusnost — dobro napisana SAX callback putanja struji uredno — nego što svaki netrivijalni potrošač uzgaja vlastiti mali automat da bi simulirao petlju koju mu nije bilo dopušteno napisati. Pomnožite to s četiri formata datoteka, od kojih je svaki povijesno imao vlastitu ulaznu točku skeniranja, i semantika filtriranja, formula i grešaka počinje među njima ići nasumce, a upravo to rasulo HotXLS je krenuo zatvoriti

Kako pull cursor mijenja vaš pozivni kod?

Vraća vam petlju, a s njom i obični Pascal tok upravljanja. TXLSRowCursor.Open prima ime datoteke ili TStream, otkriva format, jednom učitava dijeljene nizove i metapodatke o stilu datuma te bira list 1. SelectSheet (1-based) ili SelectSheetByName preusmjerava na drugi radni list i vraća kurzor na prije-prvog. FindFirst i FindNext zatim se pozicioniraju na sljedeći popunjeni redak — redovi bez dekodirajućih ćelija preskaču se, pa RowIndex može skakati — a trenutni redak izložen je kao CellCount, Cells[] i ValueByCol[], svi 1-based na osi stupaca. Izlaz iz petlje jest Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // preskoči zaglavni pojas
    Cursor.IncludeColumn(1);     // dekodiraj samo ova dva stupca
    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;               // obični Break; bez zastavice prekida, bez sentinela
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktor završava prolaz
  end;
end;

Projekcija i raspon postavljaju se prije prolaza, a ne filtriraju poslije. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates i DetectTextTypes svi se poštuju unutar pozadinskih sustava, pa neodabrani stupac nikad ne alocira svoju vrijednost, niz formule ili rich-text sadržaj na prvom mjestu — regresijski paket to dokazuje formulama od 16 KiB i predmemoriranim nizovima koji se nikad ne materijaliziraju kad njihov stupac nije projiciran. Te opcije namjerno su zamrznute dok je prolaz aktivan i ponovno postaju upisive pri EOF, na SelectSheet ili nakon Close, pa jedno skeniranje nikad ne može pomiješati dva ugovora o dekodiranju. Ako trebate samo popis listova, a ne retke, učitavanje samo metapodataka i selektivno učitavanje listova jeftinija je ulazna točka

Jedan backend po formatu, po jedna skenirajuća petlja

Svaki format ima točno jedan skener naprijed unutar HotXLS-a, a i pull cursor i callback čitač pogone taj isti skener. TXLSXForwardRowBackend jest jedini SAX automat radnog lista za dijelove listova ECMA-376 Part 1 §18.3, drži XML čitač, tablicu dijeljenih formula i parser rich-texta, i napreduje do točno jedne fizičke granice <row> po pozivu. TXLSBiffForwardParser posjeduje globalse, izbor lista i napredovanje retka za tok zapisa [MS-XLS]; učiniti ga pauzirajućim proizvelo je najstrože ograničenje u cijelom dizajnu, jer je predmemorirana string formula zapis Formula kojeg odmah slijedi zapis String, pa točka suspendiranja po retku nikad ne smije pasti između njih dvaju. TXLSForwardTextBackend drži čitač svjestan BOM-a, aktivni graničnik i jedan logički zapis — CSV njuši zarez, točku-zarez, tabulator ili cjev iz prvog zapisa ignorirajući znakove u navodnicima, a višeredna navodnična polja spajaju se s #10 pa broj retka prati logičke zapise, a ne fizičke nove retke. TXLSForwardOdsBackend drži jedan fizički predložak retka za tablice OpenDocument §9, tretira table:number-rows-repeated kao preostali brojač, a ne kao širenje, i napreduje preko pokrivenih ćelija bez ispuštanja vrijednosti. Streaming direct reader dijeli isti učitavač dijeljenih nizova i stilova datuma

HotXLS pull row cursor koji usmjerava na jedan skener naprijed po formatu, SAX backend za XLSX, parser zapisa za BIFF, tekstualni backend koji njuši graničnik i ODS predložak retka, s callback čitačem konfiguriranim iznad kao adapterom
Svaki format ima točno jedan skener naprijed, i pull cursor i callback čitač pogone isti taj skener, pa se semantika filtriranja i grešaka ne može rasuti

Zašto šest stanja umjesto jedne Eof zastavice?

Jer jedan boolean čini četiri različite situacije nerazlučivima, a pozivatelji pogađaju krivo kod svih njih. TXLSRowCursorState ih imenuje izrijekom

  • xrcsClosed — nijedan izvor nije otvoren
  • xrcsBeforeFirst — otvoreno ili preusmjereno, još nijedan redak nije pročitan
  • xrcsActive — stoji se na valjanom retku
  • xrcsEof — list je potrošen do kraja
  • xrcsCancelled — pozivatelj je namjerno zaustavio prolaz
  • xrcsFaulted — prolaz je pao i izvorna iznimka je podignuta

To posljednje razlikovanje jest ono što se računa u produkciji. Nedostajući dio radnog lista ili neuspjeli početak prolaza zadržava svoj EReadError i premješta kurzor na xrcsFaulted; nikad se ne degradira u običan False koji bi pozivatelj pročitao kao „ovaj je list bio prazan". Cancel je namjerno uži od Close: zatvara backend trenutnog radnog lista i njegov inflate podtok te poništava valjanost trenutnog retka, ali ne otpušta ZIP arhivu ni izvorni tok, a dvostruki poziv je no-op. Nakon otkazivanja nastavljate izričitim pozivom SelectSheet — kurzor vam neće tiho ponovno pokrenuti prolaz. Vlasništvo nad tokom slijedi isto obrambeno pravilo: xsoBorrowed je zadano i vraća poziciju toka pri zatvaranju, xsoOwned prenosi vlasništvo tek nakon što Open već uspije, pa neuspjeli open nikad ne oslobađa tok koji pozivatelj još drži

Šest stanja HotXLS kurzora redaka s prijelazima među njima, pokazuje Cancel koji aktivni prolaz premješta u otkazano, neuspjeli početak prolaza koji ga premješta u neuspjelo, i kako oba ostaju različita od kraja lista
Šest imenovanih stanja održava prazan list, namjerni prekid i neuspjeli prolaz razlučivima, što jedan Eof boolean ne može
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: kurzor nikad ne oslobađa Src, a Close vraća
      // poziciju koju je tok imao kad je Open pozvan
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // zatvara backend radnog lista i njegov
            Break;           // inflate podtok; idempotentno
          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;                // još je naš, još vrijedi, pozicija vraćena
  end;
end;

Posuđivanje trenutnog retka bez kopiranja

IXLSRowCursorView predaje redak drugoj rutini bez dupliciranja polja ćelija. Prikaz čuva dijeljeni čuvar koji nosi pokazivač kurzora plus UInt64 brojač generacije; napredovanje, biranje lista, otkazivanje, zatvaranje i uništavanje kurzora svi povećavaju tu generaciju, a uništavanje dodatno briše vlasnika čuvara. Pa zastarjeli prikaz ne može čitati oslobođenu memoriju: Valid je sonda bez iznimki koju možete zvati u svako doba, dok svaki drugi član prvo validira i podiže EXLSRowCursorViewInvalidated. Budite iskreni glede što je ovaj ugovor — to je doživotni fail-fast, a ne garancija sigurnosti dretvi, i ne ovlašćuje čitanje retka iz druge dretve dok prva napreduje kurzorom

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // posuđuje; polje ćelija se ne kopira
      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 čitanja zadržavaju
        else if Cell.Kind = xdkEmpty then //   predmemorirani rezultat, a ne tokene
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank jesu stvarne ćelije
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Sučelje nadživi petlju, ali redak iza njega ne
  if not View.Valid then    // Valid nikad ne podiže iznimku; Cells[] sada bi
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes i što smije dokazati

PeakRowBufferedBytes postoji da bi pokazao da memorija prati širinu retka, a ne broj redaka. Akumulira zapise ćelija, Variante, nizove formula i rich-text sadržaje trenutnog izlaznog retka i uključuje skup radnog formata — CSV logički zapis, ODS fizički predložak retka, BIFF vršni zapis ili XLSX sirovu ćeliju koja se upravo dekodira. Čitajte ga uz SheetPassesStarted, koji broji koliko je prolaza radnih listova stvarno počelo. Dvije napomene drže ovo iskrenim: brojka je procjena, ne točno računovodstvo hrpe, i monotona je od zadnjeg Open, pa je alat za otklanjanje grešaka i regresiju, a ne živi mjerač. Za širu sliku gdje odlazi vrijeme i bajtovi na vrlo velikim knjigama, pogledajte performanse velikih radnih knjiga u Delphiju

HotXLS usporedba pokazuje učitavanje cijelog lista koje svaki redak drži rezidentnim nasuprot pull cursoru koji drži samo trenutni redak plus jedan skup radnog formata, što PeakRowBufferedBytes akumulira i izvještava
PeakRowBufferedBytes akumulira trenutni izlazni redak plus skup radnog formata, pa memorija prati koliko je redak širok, a ne koliko redaka list ima

Push čitač postao je adapter, i što kurzor neće učiniti

TXLSForwardReader više ne nosi zasebne ulazne točke skeniranja za XLSX, BIFF i tekst. Konfigurira kurzor, prolazi ga i prevodi trenutni redak u događaje OnSheet i OnCell, pa dvije fasade više ne mogu ići nasumce u filtriranju, stanju formula ili obradi grešaka. Dvije posljedice vrijedi znati prije nadogradnje: callback SheetIndex sada je uniformno 1-based na TXLSForwardReader (TXLSDirectReader zadržava svoj postojeći 0-based dogovor o događajima), a OnSheet se aktivira prije SelectSheet, pa postavljanje SkipSheet znači da se dio radnog lista nikad ne otvara ni ne dekompresira. Granice su jednako izričite: radnu knjigu ne smijete mijenjati dok je prolaz aktivan, otkazivanje zahtijeva izričito ponovno pokretanje, a BIFF forward putanja nikad ne dekompilira tokene formula, pa klasične ćelije formula prijavljuju HasFormula true uz FormulaTextAvailable false i predaju vam predmemorirani rezultat umjesto da izmisle prazan niz formule. Kurzor redaka i njegov adapter prošli su 1.298 provjera na Delphi Win32 i Win64 plus C++Builder 37.0 Win64 statički paket

Ako vagate pull cursor protiv učitavača koji sada imate, pitanje nije koji brže parsira nego koji vam dopušta da napišete uvjet izlaska koji stvarno trebate. Pune pojedinosti komponente, podržane verzije IDE-a i licenciranje nalaze se na stranici HotXLS Delphi komponente za proračunske tablice