Tehnični članak

Vlečni kazalec vrstic za XLS, XLSX, ODS in CSV v Delphiju

HotXLS bere vire .xls, .xlsx, .xlsm, .ods, CSV in TSV prek enega samega vlečnega kazalca vrstic, TXLSRowCursor, katerega FindFirst in FindNext napredujeta eno logično vrstico naenkrat, v pomnilniku pa ostane le ta vrstica. Šestvrednostni stroj stanj ločuje pred-prvim od EOF, preklicanega in okvarjenega, starejši bralnik s povratnimi klici pa je zdaj vmesnik nad istim kazalcem

Scenarij je znan vsakomur, ki je oddal funkcijo uvoza. Pride 200-megabajtni .xlsx, priključite ročjevalnik OnCell, prva zahteva po »preberi ga« pa je »ustavi se po prvih sto reverznih knjiženjih«. Zdaj se oblika vaše kode bori z vami: zanka živi znotraj knjižnice, vaš ročjevalnik mora dvigniti zastavico, vsak nadaljnji povratni klic se še vedno sproži, dokler razčlenjevalnik ne opazi, nakopičeno stanje — koliko zadetkov doslej, kateri stolpec se je ujel, kaj napraviti naprej — pa mora živeti v poljih na razredu, ki obstaja samo zato, da povratnemu klicu da kam sede. Nič od tega ni težava razčlenjevanja. Je težava nadzora toka, in prav to odstrani vlečni kazalec

Kaj povratni klic potiska dejansko stane pri 200 MB

Potisk obrne nadzor, obrat pa je točno tisto, česar klicatelj, ki filtrira ali združuje, ne more privoščiti. Z API-jem povratnih klicev knjižnica lasti zanko, zato klicatelj ne more uporabiti Break, ne more preplesti dveh virov, ne more predati bralnika rutini, ki pričakuje, da bo gnana, in ne more izraziti »oglej si naslednjo vrstico, preden se odločiš«, brez medpomnjenja. Cena ni prepustnost — dobro napisana pot SAX povratnih klicev se pretaka v redu — je pa v tem, da vsak netrivialen porabnik izraste svoj majhen stroj stanj, da simulira zanko, ki je ni smel napisati. To pomnožite s štirimi formati datotek, vsak zgodovinsko s svojo vstopno točko pregledovanja, in semantika filtriranja, formul in napak začne med njimi razhajati — točno ta zdrs si je HotXLS zadala zapreti

Kako vlečni kazalec spremeni vaš klicajoč kodo?

Zanko vrne vam, s tem pa običajni nadzor toka Pascal. TXLSRowCursor.Open sprejme ime datoteke ali TStream, zazna format, enkrat naloži skupne nize in metapodatke o datumskem slogu ter izbere list 1. SelectSheet (z eniškim osnovanjem) ali SelectSheetByName ponovno usmeri na drug delovni list in ponastavi kazalec na pred-prvi. FindFirst in FindNext nato postavita na naslednjo naseljeno vrstico — vrstice brez dekodirljivih celic se preskočijo, zato se lahko RowIndex poskoki — trenutna vrstica pa je izpostavljena kot CellCount, Cells[] in ValueByCol[], vse eniško osnovane na osi stolpcev. Izstop iz zanke je Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // preskoči pas glave
    Cursor.IncludeColumn(1);     // dekodiraj le ta dva stolpca
    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čajen Break; brez zastavice prekinitve, brez čuvaja
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktor konča prehod
  end;
end;

Projekcija in obseg sta nastavljena pred prehodom, ne filtrirana nato. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates in DetectTextTypes se vsi spoštujejo znotraj zaledij, tako da neizbrani stolpec svoje vrednosti, niza formule ali tovora bogatega besedila sploh ne dodeli — preizkusna zbirka regresij to dokaže s formulami 16 KiB in predpomnjenimi nizi, ki se nikoli ne materializirajo, ko njihov stolpec ni projiciran. Te možnosti so med dejavnim prehodom namenoma zamrznjene in spet postanejo zapisljive ob EOF, ob SelectSheet ali po Close, zato en pregled nikoli ne more zmešati dveh dekodirnih dogovorov. Če potrebujete le inventar listov in ne vrstic, je nalaganje samo metapodatkov in izbirnih listov cenejša vstopna točka

Eno zaledje na format, vsako s svojo zanko pregledovanja

Vsak format ima v HotXLS točno en napredni pregledovalnik, in vlečni kazalec ter bralnik s povratnimi klici oba gneta tisti isti pregledovalnik. TXLSXForwardRowBackend je edini SAX stroj stanj delovnega lista za dele listov ECMA-376 Part 1 §18.3, ki drži bralnik XML, tabelo skupnih formul in razčlenjevalnik bogatega besedila, in napreduje na točno eno fizično mejo <row> na klic. TXLSBiffForwardParser lasti globalne spremenljivke, izbiro listov in napredovanje vrstic za zapisni tok [MS-XLS]; da je bil naredljiv za pavzo, je dalo najostrejšo omejitev v celotni zasnovi, ker je predpomnjena nizovna formula zapis Formula, ki mu takoj sledi zapis String, torej točka vmesne zaustavitve na vrstico ne sme nikoli pristati med njima. TXLSForwardTextBackend drži bralnik, pozoren na BOM, dejavno ločilo in en logični zapis — CSV povoha vejico, podpičje, tabulator ali cev iz prvega zapisa, pri tem pa prezre znake v navedkih, večvrstična navedena polja pa se spojijo s #10, tako da številka vrstice sledi logičnim zapisom in ne fizičnim novim vrsticam. TXLSForwardOdsBackend drži eno samo fizično predlogo vrstice za tabele OpenDocument §9, table:number-rows-repeated obravnava kot preostali števec in ne kot razširitev, ter napreduje mimo pokritih celic brez izdajanja vrednosti. pretočni neposredni bralnik si deli isti nalagalnik skupnih nizov in datumskih slogov

Vlečni kazalec vrstic HotXLS, ki se razpošilja na en napredni pregledovalnik na format: SAX zaledje za XLSX, razčlenjevalnik zapisov za BIFF, besedilno zaledje, ki povoha ločilo, in predloga vrstice ODS, na vrhu pa nastavljen bralnik s povratnimi klici kot vmesnik
Vsak format ima točno en napredni pregledovalnik, vlečni kazalec in bralnik s povratnimi klici pa gneta isti pregledovalnik, zato se semantika filtriranja in napak ne more razhajati

Zakaj šest stanj namesto ene zastavice Eof?

Ker en sam boolean naredi štiri različne razmere nerazločljive in se klicatelji pri vseh zmotijo. TXLSRowCursorState jih poimenuje izrecno

  • xrcsClosed — noben vir ni odprt
  • xrcsBeforeFirst — odprt ali ponovno usmerjen, še nobena vrstica prebrana
  • xrcsActive — stoji na veljavni vrstici
  • xrcsEof — list je porabljen do konca
  • xrcsCancelled — klicatelj je prehod namenoma ustavil
  • xrcsFaulted — prehod je spodletel in izvirna izjema je bila sprožena

Zadnja ločnica je tista, ki šteje v produkciji. Manjkajoči del delovnega lista ali spodletel začetek prehoda obdrži svoj EReadError in premakne kazalec v xrcsFaulted; nikoli ni degradiran v goli False, ki bi ga klicatelj prebral kot »ta list je bil prazen«. Cancel je namenoma ožji od Close: zapre zaledje trenutnega delovnega lista in njegov podtok inflate ter razveljavi trenutno vrstico, ne sprosti pa arhiva ZIP niti izvornega toka, dvojni klic pa je brez učinka. Po preklicu nadaljujete z izrecnim klicem SelectSheet — kazalec prehoda ne bo tiho znova zagnal v vašem imenu. Lastništvo toka sledi istemu obrambnemu pravilu: xsoBorrowed je privzeto in ob zaprtju obnovi položaj toka, xsoOwned pa prenese lastništvo šele, ko je Open že uspel, zato spodletelo odpiranje nikoli ne sprosti toka, ki ga klicatelj še drži

Šest stanj vlečnega kazalca vrstic HotXLS s prehodi med njimi: Cancel premakne dejaven prehod v preklicano, spodletel začetek prehoda v okvarjeno, in kako ostajata obe ločeni od konca lista
Šest poimenovanih stanj ohranja prazen list, namenoma ustavitev in spodletel prehod razločljive, česar en sam boolean Eof ne zmore
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: kazalec nikoli ne sprosti Src, Close pa obnovi
      // položaj, ki ga je tok imel, ko je bil poklican Open
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // zapre le pogon delovnega lista in njegov
            Break;           // podtok inflate; 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;                // še vedno naš, še vedno veljaven, položaj obnovljen
  end;
end;

Izposoja trenutne vrstice brez kopiranja

IXLSRowCursorView izroči vrstico drugi rutini, ne da bi podvajal tabelo celic. Pogled shrani deljenega varuha, ki hrani kazalec na kazalec in števec generacij UInt64; napredovanje, izbira lista, preklic, zapiranje in uničenje kazalca vsi povečajo to generacijo, uničenje pa poleg tega počisti lastnika varuha. Torej zastarel pogled ne more brati sproščenega pomnilnika: Valid je preizkus brez izjem, ki ga lahko pokličete kadarkoli, vsak drug član pa najprej preveri veljavnost in sproži EXLSRowCursorViewInvalidated. Bodite iskreni glede tega, kaj je ta dogovor — to je hitro odpovedoče preverjanje življenjske dobe, ne zagotovilo varnosti niti, in ne dovoljuje branja vrstice iz druge niti, medtem ko prva napreduje kazalec

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // izposoja; nobena tabela celic 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)     // napredna branja BIFF obdržijo
        else if Cell.Kind = xdkEmpty then //   predpomnjen rezultat, ne žetonov
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank so prave celice
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Vmesnik preživi zanko, vrstica za njim pa ne
  if not View.Valid then    // Valid nikoli ne sproži izjeme; Cells[] pa bi jo zdaj
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes in kaj sme dokazati

PeakRowBufferedBytes obstaja, da pokaže, da pomnilnik sledi širini vrstice in ne številu vrstic. Nabira zapise celic, Variant, nize formul in tovora bogatega besedila trenutne izhodne vrstice ter vključi specifičen delovni nabor formata — logični zapis CSV, fizično predlogo vrstice ODS, vrh zapisov BIFF ali surovo celico XLSX, ki se pravkar dekodira. Berite ga skupaj s SheetPassesStarted, ki šteje, koliko prehodov delovnih listov se je res začelo. Dve opombi ohranjata to iskreno: številka je ocena, ne točno knjigovodstvo kopice, in je monotonska od zadnjega Open, zato je orodje za razhroščevanje in regresije in ne živi merilnik. Za širšo sliko, kam gre čas in bajti na zelo velikih zvezkih, glejte zmogljivost velikih delovnih zvezkov v Delphiju

Primerjava HotXLS: nalaganje celega lista obdrži vsako vrstico v prijemu, vlečni kazalec pa drži le trenutno vrstico plus en delovni nabor formata, kar je tisto, kar PeakRowBufferedBytes nabira in poroča
PeakRowBufferedBytes nabira trenutno izhodno vrstico plus specifični delovni nabor formata, zato pomnilnik sledi temu, kako široka je vrstica, in ne temu, koliko vrstic ima list

Bralnik potiska je postal vmesnik, in česa kazalec ne bo storil

TXLSForwardReader ne nosi več ločenih vstopnih točk pregledovanja XLSX, BIFF in besedila. Nastavi kazalec, ga prehodi in trenutno vrstico prevede v dogodke OnSheet in OnCell, zato se dve fasadi ne moreta več razhajati pri filtriranju, stanju formul ali obravnavi napak. Dve posledici sta vredni poznavanja, preden nadgradite: povratni klic SheetIndex je zdaj na TXLSForwardReader povsod eniško osnovan (TXLSDirectReader obdrži svoj obstoječi dogovor dogodkov, osnovan na nič), OnSheet pa sproži pred SelectSheet, tako da nastavitev SkipSheet pomeni, da del delovnega lista sploh nikoli ni odprt ali dekomprimiran. Meje so enako izrecne: delovnega zvezka se med dejavnim prehodom ne sme spreminjati, preklic zahteva izrecen ponoven zagon, napredna pot BIFF pa nikoli ne dekompilira žetonov formul, zato klasične celice s formulami poročajo HasFormula true s FormulaTextAvailable false in vam izročijo predpomnjeni rezultat namesto da bi izmislile prazen niz formule. Vlečni kazalec in njegov vmesnik sta opravila 1.298 preizkusov na Delphi Win32 in Win64 ter statičnem paketu C++Builder 37.0 Win64

Če tehtate vlečni kazalec proti nalagalniku, ki ga imate zdaj, vprašanje ni, kateri se razčlenjuje hitreje, temveč kateri vam dovoli zapisati izstopni pogoj, ki ga dejansko potrebujete. Podrobnosti komponente, podprte različice IDE in licenciranje so na strani komponente preglednic HotXLS za Delphi