Tehnični članek

Pisanje XLSX z milijonom vrstic v Delphiju pri konstantnem pomnilniku

Opravilo za poročanje deluje brez težav eno leto. Zgradi zvezek, napolni list s tistim, kar vrne poizvedba, in ga shrani. Nato stranka s petletno zgodovino zahteva popoln izvoz, število vrstic preseže milijon in proces odpove z napako pomanjkanja pomnilnika, še preden datoteka doseže disk. S kodo ni bilo nič narobe. Celoten zvezek je hranila v RAM-u, da ga je lahko serializirala na koncu, in pomnilnik, ki ga je potrebovala, je rastel skupaj s številom vrstic, ki jih je morala zapisati

Rešitev ni večja naprava. To je drug model pisanja. Pretočni neposredni zapisovalnik v HotXLS oddaja paket OOXML postopno, ko prihajajo vrstice, tako da pomnilnik, ki ga uporablja, ni odvisen od tega, koliko vrstic zapišete. Je pisalna stran nasproti pretočnega bralnika: kot bralnik hodi po ogromnem listu brez gradnje drevesa celic, tako zapisovalnik ustvarja enega brez gradnje drevesa celic

Zakaj normalna pot shranjevanja raste s podatki

Redna pot TXLSXWorkbook najprej zgradi popoln objektni model. Vsaka celica z njeno vrednostjo, vrsto in sklicem na slog živi kot objekt v pomnilniku, dokler ne pokličete shranjevanja, ko se celotno drevo serializira v paket. Ta model je pravi, ko želite prebrati list, ga urediti, znova izračunati in zapisati nazaj, ker je naključni dostop do katere koli celice natanko tisto, kar potrebuje urejanje. Napačen je, ko vrstice zlivate v eno smer in nikoli ne gledate nazaj, ker plačate, da ohranite vsako vrstico rezidentno brez koristi. Milijon vrstic objektov je milijon vrstic objektov, ne glede na to, ali jih kdaj znova obiščete

Pretočni zapisovalnik drevo odstrani. Takoj ko je celica zapisana, postane bajti v delu delovnega lista in ti bajti so predani izhodnemu zipu. Tok delovnega lista je edini medpomnilnik, ki raste, in raste na izhodni strani, ne kot živi objekti Delphi na kopici. Kar ostane rezidentno, je fiksna količina knjiženja: imena listov, nekaj zastavic, trenutna številka vrstice, štetje celic. Ta nabor se ne spremeni med prvo in desetmilijonsko vrstico

Tabela skupnih nizov je past, vgrajeni nizi so izhod

Večina pretočnih zapisovalnikov XLSX dobro deluje, dokler ne naletijo na besedilo. Format OOXML navadno shranjuje nize v tabeli skupnih nizov: vsak ločen niz je zapisan enkrat v ločen del, in vsaka celica, ki vsebuje ta niz, nosi indeks v tabelo namesto besedila. To je dobra prostorska optimizacija za datoteke, polne ponavljajočih se oznak, in je privzeta vrednost, ki jo uporablja navadna pot shranjevanja. Problem za pretočni zapisovalnik je brutalen. Da bi deduplicirale, mora tabela ostati rezidentna za celotno opravilo, ker katera koli vrstica, ki prihaja, se morda ponovi niz iz vrstice, ki je bila že zapisana, in le popolna pomnilniška preslikava videnih nizov lahko dodeli pravi indeks. Torej je ena struktura, ki je pretočni zapisovalnik ne more pretakati, ravno tista, ki naj bi datoteko naredila majhno. Besedilni podatki premagajo pretakanje, ki ste ga prišli iskati

Neposredni zapisovalnik se tabeli povsem izogne. Nizi so zapisani v vrstici, kot celice t="inlineStr", katerih besedilo sedi neposredno v celici z elementom <is><t>. Ni tabele za kopičenje in ni preslikave videnih nizov za ohranitev, zato besedilni stolpci ne stanejo nič več pomnilnika kot numerični. Kompromis je jasen in ga je vredno navesti odkrito. Vgrajeni nizi ponavljajo isto besedilo povsod, kjer se pojavi, zato je datoteka z veliko enakimi oznakami večja na disku kot enakovredna s skupnimi nizi. Porabite velikost datoteke za konstantni pomnilnik. Za enosmerni izvoz je to prava stran kompromisa, in zip stiskanje vsekakor absorbira velik del ponavljanja pri izhodu

Tabela slogov prispe na koncu z enim formatom datuma

Slogi predstavljajo enako napetost kot nizi. Zvezek sklicuje svoje oblikovanje prek dela slogov in pretočni zapisovalnik ne more ohraniti rastoče palete slogov korak za korakom s celicami, ki jih je že odstranil. Neposredni zapisovalnik odgovori na to tako, da ohranja tabelo slogov majhno in fiksno ter jo oddaja ob zaprtju in ne vnaprej. En privzeti format celice pokriva navadne celice. En format datumske številke pokriva datume, registriran s kodo formata yyyy-mm-dd na znani poziciji v seznamu formatov celic

Ta format datuma je razlog, zakaj obstaja WriteDateTime kot lasten klic. Excel nima lastne vrste datuma; datum je številka, ki nosi format datuma. WriteDateTime zapiše vrednost kot navadna zaporedna številka in označi celico z enim slogom datuma, tako da preglednica prikaže datum namesto petmestnega celega števila. Zaporedna vrednost, ki jo zapiše, je pomembna za povratno pretvarjanje. Vrednost TDateTime shrani neposredno v sistemu datumov 1900, kar je ista konvencija, ki jo uporablja redna pot shranjevanja TXLSXWorkbook. Ker se obe poti strinjata o zaporedni vrednosti, se datoteka, ki jo ustvari pretočni zapisovalnik, prebere nazaj prek bralnika HotXLS in se odpre v Excelu z datumi, ki ustrezajo vašim nameravanjem, brez zamika ali presenečenja glede referenčne točke med zapisovalnikom in bralnikom

Vrstni red je obvezen, ker bajti so že odšli

Pretakanje kupi svoj pomnilniški profil z enim pravilom, ki ga morate upoštevati. Izhod se oddaja sproti in ga ni mogoče ponovno obiskati, zato mora biti vse zapisano v vrstnem redu, v katerem se pojavi v datoteki. Znotraj vrstice gredo celice v naraščajočem vrstnem redu stolpcev. Znotraj lista gredo vrstice v naraščajočem vrstnem redu. Ni medpomnilnika, ki bi zapisovalniku omogočal razvrščanje celic za vas, ker je vrstica, ki ste jo zaprli trenutek prej, že bajti v zip toku in ni več dosegljiva. Podajte stolpec 5 in nato stolpec 2 v isti vrstici in izhod bo napačen, ker zapisovalnik preprosto oddaja, kar mu date, v zaporedju, v katerem mu ga date

API vrstice ima majhno udobje za pogost primer. AddRow sprejme 1-osnovan indeks vrstice, toda podajanje 0 pomeni vzemi naslednjo vrstico za prejšnjo, tako da zaporedno polnjenje ni treba slediti in podajati naraščajočega števca. Vsak AddRow zapre vrstico pred njim in vsak AddSheet zapre list pred njim, tako da nikoli eksplicitno ne zaključite vrstice ali lista. Začnete naslednjega in zapisovalnik za vas zaključi odprto strukturo

Ubežanje se obravnava tam, kjer besedilo vstopi v XML

Vsako besedilo, ki ga napišete, postane del XML dokumenta, zato mora biti pet vnaprej določenih XML entitet ubežanih ali pa je paket neveljaven v trenutku, ko vrednost vsebuje ampersand ali kotno oklepalo. Zapisovalnik ubegne &, <, >, " in ' za vas tako na besedilu vgrajenega niza kot na besedilu formule -- na obeh mestih, kjer znaki, ki jih posreduje klicatelj, pristanejo v označevanju. Posredujete surov WideString in zapisovalnik ga naredi varnega. Ime izdelka, kot je Smith & Co <Ltd>, ali formula, ki sklicuje na ime lista z narekovaji, pride ven kot dobro oblikovan XML brez kakršnega koli ubežanja z vaše strani

Življenjski cikel in zakaj Destroy še vedno zapre

Zaključitev paketa je tisto, kar zapiše del zvezka, del slogov, dele vrst vsebine in razmerij ter nazadnje centralni imenik zip. To delo se zgodi v Close. Paket, ki ni nikoli zaprt, je nepopoln zip, ki ga noben program za preglednice ne bo odprl, zato zapiranje ni izbirno čiščenje -- to je korak, ki naredi datoteko veljavno. Da bi se zavaroval pred pozabljenim Close v poti napake, Destroy izvede zapiranje po najboljših zmožnostih, če je paket še vedno odprt, tako da sprostitev zapisovalnika ne pusti puščanja v spodnjem zip objektu, tudi ko je izjema preskočila ekspliciten klic. Zanesljiv vzorec je še vedno navadni Delphi: pišite znotraj try, pokličite Close in sprostite v finally

Pretakanje velikega lista od začetka do konca

Oblika opravila je začni, dodaj list, ulij vrstice, zapri. Spodnji primer zapiše vrstico glave in nato dolgo serijo tipiziranih podatkovnih vrstic, ki mešajo nize, številke, formulo brez predpomnjene vrednosti in datum. Pomnilnik, ki ga porabi za deset vrstic in za deset milijonov vrstic, je enak, ker vsaka celica odide v zip tok takoj, ko je zapisana

uses
  lxDirectWrite;

procedure StreamReport(const Path: string; RowCount: Integer);
var
  W: TXLSDirectWriter;
  I: Integer;
begin
  W := TXLSDirectWriter.Create;
  try
    W.BeginFile(Path);
    W.AddSheet('Sales');

    // Header row, written in ascending column order
    W.AddRow(1);
    W.WriteString(1, 'Item');
    W.WriteString(2, 'Qty');
    W.WriteString(3, 'Price');
    W.WriteString(4, 'Total');
    W.WriteString(5, 'Date');

    // Data rows; pass 0 to AddRow to take the next row automatically
    for I := 1 to RowCount do
    begin
      W.AddRow(0);
      W.WriteString(1, 'Item ' + IntToStr(I));
      W.WriteNumber(2, I);
      W.WriteNumber(3, 1.5 + (I mod 10));
      W.WriteFormula(4, Format('B%d*C%d', [I + 1, I + 1]));
      W.WriteDateTime(5, EncodeDate(2026, 1, 1) + I);
    end;

    W.Close;                       // finalises the package
  finally
    W.Free;
  end;
end;

Drugi list je preprosto drug AddSheet, preden nadaljujete, in zapisovalnik zapre prvi list, ko odpre drugi. Logične zastavice uporabljajo WriteBoolean, ki zapiše tipicirano logično celico in ne besedila "True". Če želite potrditi, da je datoteka zvočna in se pravilno pretvori nazaj, lastnost CellCount poroča, koliko celic je bilo zapisanih, in branje rezultata nazaj s pretočnim bralnikom bi moralo poročati isti skupni znesek

  // A second sheet of typed flags after the data sheet above
  W.AddSheet('Flags');
  W.AddRow(1);
  W.WriteString(1, 'Name');
  W.WriteString(2, 'Active');
  W.AddRow(0);
  W.WriteString(1, 'alpha');
  W.WriteBoolean(2, True);

  WriteLn(Format('wrote %d cells', [W.CellCount]));

Pisanje v tok namesto v datoteko je enaka koda z BeginStream namesto BeginFile, kar strežniku omogoča pošiljanje zvezka v HTTP odziv ali pomnilniški tok brez začasne datoteke na disku. Zapisovalnik ne poseduje toka, ki ga posredujete, zato ohranite nadzor nad njegovim življenjskim ciklom

Ko je opravilo strežniška končna točka, ki gradi zvezke na zahtevo, vzorci v pretočnem pisanju za strežniška in paketna opravila prikazujejo, kako to vključiti v upravljalnik zahtev in načrtovani izvoz. Ko je vprašanje širši strošek zelo velikih zvezkov -- tako branja kot pisanja -- zmogljivost velikih zvezkov v Delphiju pokriva, kam dejansko gresta čas in pomnilnik. Pretočni neposredni zapisovalnik se dobavi kot del Komponente HotXLS za Delphi in C++Builder, skupaj s celotnimi API-ji za branje, urejanje in shranjevanje, obravnavanimi drugje na tem blogu