Tehnički članak

Pull row kursor za XLS, XLSX, ODS i CSV u Delphi-ju

HotXLS čita .xls, .xlsx, .xlsm, .ods, CSV i TSV izvore kroz jedan jedini pull row kursor, TXLSRowCursor, čiji FindFirst i FindNext pomeraju jedan logički red po putu dok samo taj red ostaje u memoriji. Mašina stanja sa šest vrednosti razdvaja stanje pre prvog reda od EOF, otkazanog i neuspešnog prolaza, a stariji čitač sa callback-ovima je sada adapter nad istim kursorom

Scenario je poznat svakome ko je isporučio funkciju uvoza. Stigne 200 MB .xlsx, priključite OnCell rukovaoca, i prvi zahtev posle „pročitaj ga" je „stani posle prvih stotinu reverznih knjiženja". Sada se oblik vašeg koda bori protiv vas: petlja živi unutar biblioteke, vaš rukovalac mora da podigne flag, svaki sledeći callback i dalje se okida dok parser ne primeti, i nakupljeno stanje — koliko pogodaka do sada, koja kolona se poklopila, šta dalje — mora živeti u poljima klase koja postoji samo da bi callback imao gde da sedi. Ništa od toga nije problem parsiranja. To je problem toka upravljanja, i upravo ga pull kursor uklanja

Šta push callback stvarno košta na 200 MB

Push obrće upravljanje, a obrtanje je upravo ono što pozivalac koji filtrira ili spaja ne može sebi da priušti. Sa callback API-jem biblioteka poseduje petlju, pa pozivalac ne može Break, ne može da ispreplće dva izvora, ne može da da čitač rutini koja očekuje da bude vođena, i ne može da izrazi „pogledaj sledeći red pre odluke" bez baferisanja. Cena nije propusnost — dobro napisan SAX callback put strimi sasvim fino — nego što svaki netrivijalni potrošač uzgaja sopstvenu malu mašinu stanja da simulira petlju koju nije smeo da napiše. Pomnožite to sa četiri formata fajla, od kojih je svaki istorijski imao svoju ulaznu tačku skeniranja, i semantika filtriranja, formula i grešaka počinje da se razilazi među njima, a upravo taj razmak HotXLS je nameravao da zatvori

Kako pull kursor menja vaš kod poziva?

Vraća vam petlju, i sa njom običan Pascal tok upravljanja. TXLSRowCursor.Open prima ime fajla ili TStream, detektuje format, jednom učitava deljene stringove i metapodatke o datumskom stilu, i bira list 1. SelectSheet (1-bazirano) ili SelectSheetByName preusmerava na drugi radni list i resetuje kursor na before-first. FindFirst i FindNext zatim pozicioniraju na sledeći naseljeni red — redovi bez dekodabilnih ćelija se preskaču, pa RowIndex može da skoči — a tekući red je izložen kao CellCount, Cells[] i ValueByCol[], svi 1-bazirani po kolonskoj osi. Izlaz iz petlje je Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // preskoči pojas zaglavlja
    Cursor.IncludeColumn(1);     // dekodiraj samo ove dve kolone
    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čan Break; bez abort flaga, bez sentinela
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktor završava prolaz
  end;
end;

Projekcija i opseg se postavljaju pre prolaza, ne filtriraju posle. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates i DetectTextTypes se svi poštuju unutar bekendova, pa neprojektovana kolona nikad ne alocira svoju vrednost, formula string ili rich-text sadržaj na prvom mestu — regresiona serija ovo dokazuje sa 16 KiB formulama i keširanim stringovima koji se nikad ne materijalizuju kada njihova kolona nije projektovana. Te opcije su namerno zamrznute dok je prolaz aktivan i ponovo postaju upisljive na EOF, na SelectSheet, ili posle Close, pa jedno skeniranje nikad ne može da pomeša dva ugovora dekodovanja. Ako vam treba samo inventar listova a ne redovi, učitavanje samo metapodataka i selektivno učitavanje listova je jeftinija ulazna tačka

Jedan bekend po formatu, po jedna skenirajuća petlja

Svaki format ima tačno jedan unapredni skener unutar HotXLS-a, i i pull kursor i callback čitač voze taj isti skener. TXLSXForwardRowBackend je jedina worksheet SAX mašina stanja za sheet delove ECMA-376 Part 1 §18.3, drži XML čitač, tabelu deljenih formula i rich-text parser, i napreduje na tačno jednu fizičku <row> granicu po pozivu. TXLSBiffForwardParser poseduje globalse, izbor lista i napredovanje reda za [MS-XLS] tok zapisa; učiniti ga pauzabilnim proizvelo je naj-oštrije ograničenje celog dizajna, jer je keširana string formula Formula zapis odmah praćen String zapisom, pa tačka suspendovanja po redu nikad ne sme da padne između njih dva. TXLSForwardTextBackend drži BOM-svestan čitač, aktivni delimiter i jedan logički zapis — CSV namiruje zarez, tačku-zarez, tab ili pipe iz prvog zapisa ignorišući citirane znakove, i višelinijski citirani poli se spajaju sa #10 tako da broj reda prati logičke zapise a ne fizičke nove linije. TXLSForwardOdsBackend drži jedan fizički šablon reda za OpenDocument §9 tabele, tretira table:number-rows-repeated kao preostali brojač a ne ekspanziju, i napreduje preko pokrivenih ćelija bez emitovanja vrednosti. Streaming direktan čitač deli isti učitalac deljenih stringova i datumskih stilova

HotXLS pull row kursor koji raspoređuje na jedan unapredni skener po formatu, SAX bekend za XLSX, parser zapisa za BIFF, tekst bekend koji namiruje delimiter i ODS šablon reda, sa callback čitačem konfigurisanim iznad kao adapter
Svaki format ima tačno jedan unapredni skener, i i pull kursor i callback čitač voze taj isti skener, pa se semantika filtriranja i grešaka ne može razilaziti

Zašto šest stanja umesto jednog Eof flaga?

Jer jedan boolean čini četiri različite situacije nerazlučivim, i pozvaoci pogađaju pogrešno kod svih njih. TXLSRowCursorState ih imenuje eksplicitno

  • xrcsClosed — nijedan izvor nije otvoren
  • xrcsBeforeFirst — otvoren ili preusmeren, još nijedan red nije pročitan
  • xrcsActive — stoji na važećem redu
  • xrcsEof — list je potrošen do kraja
  • xrcsCancelled — pozivalac je namerno zaustavio prolaz
  • xrcsFaulted — prolaz je pao i originalni izuzetak je dignut

To poslednje razlikovanje je ono što biti u produkciji. Nedostajući worksheet deo ili neuspeo početak prolaza zadržava svoj EReadError i pomera kursor na xrcsFaulted; nikad nije unovažen u običan False koji bi pozivalac pročitao kao „ovaj list je bio prazan". Cancel je namerno uži od Close: zatvara tekući worksheet bekend i njegov inflate podtok i poništava tekući red, ali ne otpušta ZIP arhivu ni izvorni tok, i pozivanje ga dva puta je no-op. Posle otkazivanja nastavljate eksplicitnim pozivom SelectSheet — kursor neće tiho restartovati prolaz u vaše ime. Vlasništvo nad tokom prati isto defanzivno pravilo: xsoBorrowed je podrazumevano i vraća poziciju toka pri zatvaranju, xsoOwned prenosi vlasništvo tek kada je Open već uspeo, pa neuspeo open nikad ne oslobađa tok koji pozivalac i dalje drži

Šest stanja HotXLS row kursora sa prelazima među njima, pokazujući Cancel koji aktivan prolaz pomera u otkazano, neuspeo početak prolaza koji ga pomera u neuspešno, i kako oba ostaju različita od kraja lista
Šest imenovanih stanja čini prazan list, namerni prekid i neuspeo prolaz razlučivim, š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: kursor nikad ne oslobađa Src, i Close vraća
      // poziciju koju je tok imao kada je Open pozvan
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // zatvara worksheet bekend i njegov
            Break;           // inflate podtok samo; 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;                // i dalje naš, i dalje važeći, pozicija vraćena
  end;
end;

Pozajmljivanje tekućeg reda bez kopiranja

IXLSRowCursorView predaje red drugoj rutini bez dupliciranja niza ćelija. Prikaz čuva deljeni guard koji drži pokazivač kursora plus UInt64 brojač generacije; napredovanje, izbor lista, otkazivanje, zatvaranje i uništavanje kursora svi povećavaju tu generaciju, i uništavanje dodatno briše vlasnika guarda. Tako zastareli prikaz ne može da čita oslobođenu memoriju: Valid je sonda bez izuzetaka koju možete zvati u svako vreme, dok svaki drugi član prvo validira i diže EXLSRowCursorViewInvalidated. Budite iskreni oko toga šta je ovaj ugovor — to je lifetime fail-fast, ne garancija bezbednosti niti, i ne daje dozvolu da se red čita iz druge niti dok prva pomera kursor

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // pozajmljuje; niz ć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 //   keširani rezultat, ne tokene
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank su prave ćelije
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Interfejs nadživljava petlju, ali red iza njega ne
  if not View.Valid then    // Valid nikad ne diže izuzetak; Cells[] sada bi digla
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes, i šta sme da dokazuje

PeakRowBufferedBytes postoji da pokaže da memorija prati širinu reda a ne broj redova. Nakuplja zapise ćelija, Variante, formula stringove i rich-text sadržaje tekućeg izlaznog reda i upliće format-specifičan radni skup — CSV logički zapis, ODS fizički šablon reda, BIFF vrh zapisa, ili XLSX sirovu ćeliju koja se trenutno dekoduje. Čitajte ga zajedno sa SheetPassesStarted, koji broji koliko je worksheet prolaza stvarno počelo. Dve napomene drže ovo pošteno: brojka je procena, ne tačno heap računovodstvo, i monoton je od poslednjeg Open, pa je to instrument za otklanjanje grešaka i regresiju pre nego živi merilac. Za širu sliku gde odlaze vreme i bajtovi na vrlo velikim sveskama, vidite performanse velikih radnih svesaka u Delphi-ju

HotXLS poređenje koje pokazuje učitavanje celog lista koje drži svaki red rezidentnim naspram pull kursora koji drži samo tekući red plus jedan format radni skup, što je ono što PeakRowBufferedBytes nakuplja i izveštava
PeakRowBufferedBytes nakuplja tekući izlazni red plus format-specifičan radni skup, pa memorija prati koliko je red širok a ne koliko list ima redova

Push čitač je postao adapter, i šta kursor neće da radi

TXLSForwardReader više ne nosi odvojene XLSX, BIFF i tekst ulazne tačke skeniranja. Konfiguriše kursor, prolazi ga, i prevodi tekući red u OnSheet i OnCell događaje, i zato dve fasade više ne mogu da se razilaze po filtriranju, stanju formula ili rukovanju greškama. Dve posledice vredi znati pre nadogradnje: callback SheetIndex je sada ujednačeno 1-baziran na TXLSForwardReader (TXLSDirectReader zadržava svoj postojeći 0-bazirani ugovor događaja), i OnSheet se okida pre SelectSheet, pa podešavanje SkipSheet znači da worksheet deo nikad nije ni otvoren ni dekomprimovan. Granice su jednako eksplicitne: radna sveska ne sme da se menja dok je prolaz aktivan, otkazivanje traži eksplicitan restart, i BIFF forward putanja nikad ne dekompajlira tokene formula, pa klasične formula ćelije izveštavaju HasFormula true sa FormulaTextAvailable false i predaju vam keširani rezultat umesto da izmišljaju prazan formula string. Row kursor i njegov adapter prošli su 1.298 provera na Delphi Win32 i Win64 plus C++Builder 37.0 Win64 statičkom paketu

Ako vagate pull kursor naspram učitača koji sada imate, pitanje koje treba postaviti nije koji parsira brže nego koji vam dozvoljava da napišete uslov izlaska koji stvarno trebate. Kompletni detalji komponente, podržane IDE verzije i licenciranje su na stranici HotXLS Delphi spreadsheet komponente