Tekninen artikkeli

Pull-rivikursori XLS-, XLSX-, ODS- ja CSV:lle Delphissä

HotXLS lukee .xls-, .xlsx-, .xlsm-, .ods-, CSV- ja TSV-lähteet yhden pull-rivikursorin kautta: TXLSRowCursor, jonka FindFirst ja FindNext etenevät yhden loogisen rivin kerrallaan, ja vain kyseinen rivi pysyy muistissa. Kuuden arvon tilakone erottaa ensimmäistä edeltävän tilan EOF:sta, perutusta ja epäonnistuneesta, ja vanhempi callback-lukija on nyt adapteri saman kursorin päällä

Tilanne on tuttu jokaiselle, joka on toimittanut tuontiominaisuuden. 200 megatavun .xlsx saapuu, kytket OnCell-käsittelijän, ja ensimmäinen vaatimus luku-ohjeen jälkeen on pysähtyä sadan ensimmäisen peruutetun kirjauksen jälkeen. Nyt koodisi muoto taistelee sinua vastaan: silmukka asuu kirjaston sisällä, käsittelijäsi joutuu nostamaan lipun, jokainen myöhempi callback laukeaa yhä, kunnes jäsennin huomaa, ja kertynyt tila — montako osumaa tähän mennessä, mikä sarake osui, mitä tehdä seuraavaksi — joutuu asumaan luokan kenttiin, joka on olemassa vain antaakseen callbackille paikan istua. Mitään siinä ei ole jäsennysongelma. Se on ohjausvirtaongelma, ja se on se, jonka pull-kursori poistaa

Mitä push-callback oikeasti maksaa 200 megatavussa

Push kääntää ohjauksen väärinpäin, ja käänteisyys on juuri se, mitä suodattava tai liittävä kutsuja ei voi maksaa. Callback-API:lla kirjasto omistaa silmukan, joten kutsuja ei voi käyttää Breakia, ei voi lomittaa kahta lähdettä, ei voi antaa lukijaa rutiinille, joka odottaa tulleensa ajettavaksi, eikä voi ilmaista kurkista seuraavaa riviä ennen päätöstä ilman puskurointia. Hintana ei ole läpäisy — hyvin kirjoitettu SAX-callback-polku suoratoistaa ihan hyvin — vaan se, että jokainen ei-triviaali kuluttaja kasvattaa pienen oman tilakoneensa simuloidakseen silmukkaa, jota ei saanut kirjoittaa. Kerro se neljällä tiedostomuodolla, joista kullakin on historiansa aikana ollut oma skannauspäätepisteensä, ja suodatus-, kaava- ja virhesemantiikka alkavat ajautua erilleen niiden välillä, mikä on täsmälleen se ajautuminen, jonka sulkemiseen HotXLS lähti

Kuinka pull-kursori muuttaa kutsukoodiasi?

Se antaa silmukan takaisin sinulle, ja mukana tavallisen Pascal-ohjausvirran. TXLSRowCursor.Open hyväksyy tiedostonimen tai TStream-olion, tunnistaa muodon, lataa jaetut merkkijonot ja päivämäärätyylimetadatan kerran ja valitsee arkin 1. SelectSheet (1-pohjainen) tai SelectSheetByName kohdistaa toiseen työarkiin ja nollaa kursorin ensimmäistä edeltävään tilaan. FindFirst ja FindNext asettuvat sitten seuraavalle täytetylle riville — rivit ilman dekoodattavia soluja ohitetaan, joten RowIndex voi hypätä — ja nykyinen rivi on näkyvillä kohteina CellCount, Cells[] ja ValueByCol[], kaikki 1-pohjaisia sarakkeakselilla. Silmukasta poistuminen on Break

var
  Cursor: TXLSRowCursor;
  Hits: Integer;
begin
  Cursor := TXLSRowCursor.Create;
  try
    Cursor.FirstRow := 2;        // ohita otsikkokaista
    Cursor.IncludeColumn(1);     // dekoodaa vain nämä kaksi saraketta
    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;               // tavallinen Break; ei abort-lippua, ei vartijaa
        end;
      until not Cursor.FindNext;
  finally
    Cursor.Free;                 // destruktori lopettaa kierroksen
  end;
end;

Projisointi ja alue asetetaan ennen kierrosta, ei suodateta jälkikäteen. Kohteet FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates ja DetectTextTypes kunnioitetaan kaikissa taustajärjestelmissä, joten valitsematon sarake ei koskaan varaa arvoaan, kaavamerkkijonoaan tai riketekstipakettiaan alun perinkään — regressiotestisarja todistaa tämän 16 KiB:n kaavoilla ja välimuistisilla merkkijonoilla, joita ei koskaan materialisoida, kun niiden saraketta ei projisoida. Nämä valinnat jäädytetään tarkoituksella kierroksen ollessa aktiivinen ja muuttuvat taas kirjoitettaviksi EOF:ssa, SelectSheetissä tai Close-kutsun jälkeen, joten yksi skannaus ei voi koskaan sekoittaa kahta dekoodaussopimusta. Jos tarvitset vain arkkilistan eikä rivejä, vain metadatan ja valikoivan arkkilatauksen kautta pääsee halvemmalla

Yksi taustajärjestelmä per muoto, yksi skannaussilmukka kullekin

Jokaisella muodolla on täsmälleen yksi eteenpäin skannaava lukija HotXLS:n sisällä, ja sekä pull-kursori että callback-lukija ajavat samaa lukijaa. TXLSXForwardRowBackend on ainoa työarkkien SAX-tilakone ECMA-376 Part 1 §18.3 -arkkiosille, ja se pitää hallussaan XML-lukijan, jaettujen kaavojen taulun ja riketekstijäsennimen, ja etenee täsmälleen yhden fyysisen <row>-rajan per kutsu. TXLSBiffForwardParser omistaa globaalit, arkin valinnan ja rivin etenemisen [MS-XLS]-tietuevirralle; sen tekeminen keskeytettäväksi tuotti terävimmän rajoitteen koko suunnittelussa, koska välimuistissa oleva merkkijonokaava on Formula-tietue, jota seuraa välittömästi String-tietue, joten rivikohtainen keskeytyskohta ei saa koskaan osua niiden väliin. TXLSForwardTextBackend pitää BOM-tietoisen lukijan, aktiivisen erottimen ja yhden loogisen tietueen — CSV nuuhkii pilkun, puolipisteen, sarkaimen tai putken ensimmäisestä tietueesta laiminlyöden lainattuja merkkejä, ja moniriviset lainatut kentät yhdistetään #10:llä, joten rivinumero seuraa loogisia tietueita eikä fyysisiä rivinvaihtoja. TXLSForwardOdsBackend pitää yhtä fyysistä rivipohjaa OpenDocument §9 -tauluille, käsittelee kohteen table:number-rows-repeated jäljellä olevan lukumääränä laajennuksen sijaan ja etenee peitettyjen solujen ohi emittoimatta arvoja. Suoratoistava suora lukija jakaa saman jaettujen merkkijonojen ja päivämäärätyylien lataajan

HotXLS:n pull-rivikursori ohjautuu yhteen eteenpäin skannaavaan lukijaan per muoto, SAX-taustajärjestelmä XLSX:lle, tietuejäsennin BIFF:lle, erottimen nuuhkiva tekstitaustajärjestelmä ja ODS-rivipohja, ja callback-lukija konfiguroituna päälle adapterina
Jokaisella muodolla on täsmälleen yksi eteenpäin skannaava lukija, ja sekä pull-kursori että callback-lukija ajavat samaa lukijaa, joten suodatus- ja virhesemantiikka eivät voi ajautua erilleen

Miksi kuusi tilaa yhden Eof-lipun sijaan?

Koska yksi totuusarvo tekee neljästä eri tilanteesta erottamattomia, ja kutsujat arvaavat väärin niissä kaikissa. TXLSRowCursorState nimeää ne eksplisiittisesti

  • xrcsClosed — mitään lähdettä ei ole avattu
  • xrcsBeforeFirst — avattu tai uudelleenkohdistettu, yhtään riviä ei vielä luettu
  • xrcsActive — seisoa kelvollisella rivillä
  • xrcsEof — arkki kulutettiin loppuun
  • xrcsCancelled — kutsuja pysäytti kierroksen tarkoituksella
  • xrcsFaulted — kierros epäonnistui ja alkuperäinen poikkeus nostettiin

Viimeinen ero on se, joka merkitsee tuotannossa. Puuttuva työarkkiosa tai epäonnistunut kierroksen aloitus säilyttää kohteen EReadError ja siirtää kursorin kohteeseen xrcsFaulted; sitä ei koskaan alenneta tavalliseksi Falseksi, jonka kutsuja lukisi tämä arkki oli tyhjä. Cancel on tarkoituksella kapeampi kuin Close: se sulkee nykyisen työarkin taustajärjestelmän ja sen inflate-alivirran ja mitätöi nykyisen rivin, mutta se ei vapauta ZIP-arkistoa eikä lähdevirtaa, ja sen kutsuminen kahdesti on ei-toiminto. Peruutuksen jälkeen jatkat kutsumalla SelectSheet-kutsua eksplisiittisesti — kursori ei käynnistä kierrosta hiljaa puolestasi. Virran omistus noudattaa samaa puolustussääntöä: xsoBorrowed on oletus ja palauttaa virran sijainnin sulussa, xsoOwned siirtää omistuksen vain, kun Open on jo onnistunut, joten epäonnistunut avaus ei koskaan vapauta virtaa, jota kutsuja yhä pitää

HotXLS-rivikursorin kuusi tilaa ja siirtymät niiden välillä, näyttäen Cancelin siirtävän aktiivisen kierroksen perutuksi, epäonnistuneen kierroksen aloituksen siirtävän sen epäonnistuneeksi ja sen, kuinka molemmat pysyvät erillään arkin lopusta
Kuusi nimettyä tilaa pitävät tyhjän arkin, tarkoituksellisen pysäytyksen ja epäonnistuneen kierroksen erotettavissa, minkä yksi Eof-totuusarvo ei voi tehdä
var
  Cursor: TXLSRowCursor;
  Src: TFileStream;
begin
  Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
  try
    Cursor := TXLSRowCursor.Create;
    try
      // xsoBorrowed: kursori ei koskaan vapauta Src:tä, ja Close palauttaa
      // sijainnin, jonka virta oli, kun Open kutsuttiin
      if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
        Exit;

      if Cursor.FindFirst then
        repeat
          if UserPressedStop then
          begin
            Cursor.Cancel;   // sulkee vain työarkin taustajärjestelmän ja sen
            Break;           // inflate-alivirran; idempotentti
          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;                // yhä meidän, yhä kelvollinen, sijainti palautettu
  end;
end;

Nykyisen rivin lainaaminen kopioimatta sitä

IXLSRowCursorView antaa rivin toiselle rutiinille monistamatta solutaulukkoa. Näkymä tallentaa jaetun vartijan, joka pitää kursoriosoittimen plus UInt64-sukupolvelaskurin; eteneminen, arkin valinta, peruutus, sulkeminen ja kursorin tuhoaminen kasvattavat kaikki kyseistä sukupolvea, ja tuhoaminen lisäksi tyhjentää vartijan omistajan. Vanhentunut näkymä ei siis voi lukea vapautettua muistia: Valid on poikkeukseton tunnistin, jota voit kutsua milloin tahansa, kun taas jokainen muu jäsen validoi ensin ja nostaa kohteen EXLSRowCursorViewInvalidated. Ole rehellinen sen suhteen, mikä tämä sopimus on — se on elinkaaren fail-fast, ei säieturvallisuustakuu, eikä se lupaa lukea riviä toisesta säikeestä, kun ensimmäinen etenee kursorissa

var
  View: IXLSRowCursorView;
  Cell: TXLSRowCursorCell;
  I: Integer;
begin
  if Cursor.FindFirst then
    repeat
      View := Cursor.CurrentRowView;      // lainaa; solutaulukkoa ei kopioida
      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 -luvut säilyttävät
        else if Cell.Kind = xdkEmpty then //   välimuistituloksen, ei tokeneita
          UseStyleOnly(Cell.StyleIndex)   // Blank / MulBlank ovat oikeita soluja
        else
          UseValue(Cell.Col, Cell.Value);
      end;
    until not Cursor.FindNext;

  // Rajapinta elää silmukan yli, mutta sen takana oleva rivi ei
  if not View.Valid then    // Valid ei koskaan nosta; Cells[] nyt nostaisi
    View := nil;            // EXLSRowCursorViewInvalidated
end;

PeakRowBufferedBytes ja mitä se saa todistaa

PeakRowBufferedBytes on olemassa osoittamaan, että muisti seuraa rivin leveyttä eikä rivien määrää. Se kasauttaa nykyisen tulosterivin solutietueet, Variantit, kaavamerkkijonot ja riketekstipaketit ja taitaa sisään muotokohtaisen työjoukon — CSV:n loogisen tietueen, ODS:n fyysisen rivipohjan, BIFF:n tietuehuipun tai XLSX:n raakasolun, jota parhaillaan dekoodataan. Lue se yhdessä kohteen SheetPassesStarted kanssa, joka laskee, kuinka monta työarkkikierrosta oikeasti alkoi. Kaksi varausta pitää tämän rehellisenä: luku on arvio, ei tarkka keotilinpito, ja se on monotoninen viimeisimmän Open-kutsun jälkeen, joten se on debuggaus- ja regressiinstrumentti eikä live-mittari. Laajemman kuvan siitä, mihin aika ja tavut menevät erittäin suurissa työkirjoissa, antaa suurten työkirjojen suorituskyky Delphissä

HotXLS-vertailu, jossa koko arkin lataus pitää jokaisen rivin muistissa vastaan pull-kursori, joka pitää vain nykyisen rivin plus yhden muodon työjoukon, mikä on se, mitä PeakRowBufferedBytes kasauttaa ja raportoi
PeakRowBufferedBytes kasauttaa nykyisen tulosterivin plus muotokohtaisen työjoukon, joten muisti seuraa, kuinka leveä rivi on, sen sijaan että laskisi, kuinka monta riviä arkissa on

Push-lukijasta tuli adapteri, ja mitä kursori ei tee

TXLSForwardReader ei enää kanna erillisiä XLSX-, BIFF- ja tekstiskannauksen sisääntulopisteitä. Se konfiguroi kursorin, kulkee sen läpi ja kääntää nykyisen rivin kohteiksi OnSheet ja OnCell -tapahtumiksi, minkä vuoksi kaksi julkisivua ei voi enää ajautua erilleen suodatuksessa, kaavatilassa tai virhekäsittelyssä. Kaksi seurausta kannattaa tietää ennen päivitystä: callbackin SheetIndex on nyt yhtenäisesti 1-pohjainen kohteessa TXLSForwardReader (TXLSDirectReader säilyttää olemassa olevan 0-pohjaisen tapahtumasopimuksensa), ja OnSheet laukeaa ennen SelectSheetia, joten kohteen SkipSheet asettaminen tarkoittaa, että työarkkiosaa ei koskaan avata tai pureta lainkaan. Rajat ovat yhtä lailla eksplisiittiset: työkirjaa ei saa muokata kierroksen ollessa aktiivinen, peruutus vaatii eksplisiittisen uudelleenkäynnistyksen, ja BIFF forward -polku ei koskaan dekompileoi kaavatokeneja, joten klassiset kaavasolut raportoivat kohteen HasFormula todeksi kohteen FormulaTextAvailable ollessa epätosi ja antavat sinulle välimuistituloksen keksimättä tyhjää kaavamerkkijonoa. Rivikursori ja sen adapteri läpäisivät 1 298 tarkistusta Delphi Win32:lla ja Win64:llä plus C++Builder 37.0 Win64 -staattisessa paketissa

Jos punnitset pull-kursoria vastaan sen lukijan kanssa, jonka sinulla nyt on, kysymys ei ole siitä, kumpi jäsentää nopeammin, vaan siitä, kumpi antaa sinun kirjoittaa poistumisehdon, jonka oikeasti tarvitset. Täydelliset komponenttitiedot, tuetut IDE-versiot ja lisensöinti ovat HotXLS Delphi -taulukkolaskentakomponentin sivulla