Preglednica z milijonom vrstic in ducatom stolpcev je povsem navaden izvoz iz opravila za poročanje baze podatkov. Odprite jo na običajen način, z nalaganjem celotnega zvezka v TXLSWorkbook, in proces mora materializirati vseh dvanajst milijonov celic kot živi objekt, preden se zažene vaša prva vrstica poslovne logike. Datoteka na disku je morda šestdeset megabajtov stisnjene XML. Drevo objektov, v katero se razširi, je večkratnik tega, in vse mora biti hkrati rezidentno, ker je model zasnovan za naključni dostop. Za poročilo, ki ga nameravate brati od zgoraj navzdol in zavreči, je to ogromna poraba pomnilnika za strukturo, ki je nikoli niste potrebovali
Skozi isto datoteko vodi druga pot. Namesto gradnje modela skenirate XML delovnega lista samo naprej, po eno celico naenkrat, in vsako celico pustite odteči, ko ste jo pogledali. Nič se ne kopiči. Pomnilnik ostaja skoraj konstanten, ne glede na to, ali ima list tisoč vrstic ali deset milijonov, ker bralnik nikoli ne drži več kot tistega dela, ki ga trenutno razčlenjuje, ter par majhnih iskovalnih tabel. To je tisto, kar počne neposredni bralnik HotXLS, in preostanek tega članka pojasnjuje, zakaj ostaja majhen in kaj vam nudi v zameno
Zakaj pomnilniški model ne skalira
Datoteka XLSX je ZIP paket delov XML, opisanih po ECMA-376. Vsak delovni list je lasten del, xl/worksheets/sheetN.xml, in v njem je vsaka vrstica element <row>, ki vsebuje elemente celic <c>. Navadna pot nalaganja prebere ta del in za vsako celico zgradi naslovljiv objekt, da lahko pozneje vprašate za Cells[12345, 7] in dobite odgovor v konstantnem času. Naključni dostop je celoten namen modela zvezka in je natanko tisto, kar naredi urejanje, vrednotenje formul in oblikovanje priročno
Cena je v tem, da naključni dostop zahteva, da je vse hkrati prisotno. Ne morete indeksirati v strukturo, ki ste jo zgradili le delno. Zato je vrhunski pomnilnik polnega nalaganja funkcija števila celic, in na listu z milijoni poseljenih celic ta funkcija pristane nekje, kjer vaša storitev ne želi biti, zlasti če na skupni napravi hkrati deluje več takšnih opravil. Ko je vzorec dostopa, ki ga dejansko potrebujete, zaporeden, plačevanje za naključni dostop pomeni plačevanje za zmožnost, ki je ne boste koristili
Skeniranje SAX samo naprej brez gradnje drevesa
Neposredni bralnik odpre ZIP paket in po vsakem delu delovnega lista hodi s potegalnim razčlenjevalnikom v slogu SAX. SAX tu pomeni, da razčlenjevalnik poroča o razčlenitvenih dogodkih, ko jih naleti -- začetni element, besedilni niz, končni element -- in nato nadaljuje. Za seboj ne ohranja nobenega drevesa vozlišč. Bralnik sledi trenutni vrstici in stolpcu iz atributov r, zbira vrsto celice, indeks sloga, vrednost in besedilo formule ob prihodu dogodkov ter ko se vidi zaključna oznaka </c>, odda eno celico in jo pozabi. Naslednja celica znova uporabi ista peščica lokalnih spremenljivk
Ker se med celicami nič ne ohrani, pomnilniški odtis ne raste s številom celic. To je lastnost, ki jo je vredno ohraniti. List z dvesto vrsticami in list z dvajset milijoni vrstic staneta bralnika enako rezidentnega pomnilnika, razlika med njima pa je le, kako dolgo traja skeniranje. Odreknete se naključnemu dostopu, ki je glavna funkcija modela, in v zameno dobite zgornjo mejo pomnilnika, ki je število celic ne more preseči
Kaj ostane rezidentno in zakaj tista dva dela
Skeniranje ni povsem brez stanja in izjeme so poučne. Dve majhni tabeli morata biti v pomnilniku ves čas trajanja, ker celica sama po sebi ne nosi dovolj informacij za interpretacijo brez njiju
Prva je tabela skupnih nizov. V SpreadsheetML besedilna celica ne shranjuje lastnega besedila. Nosi t="s" in numerično vsebino, ki je indeks v xl/sharedStrings.xml -- en sam deduplicirani seznam vsakega ločenega niza v zvezku. To je dober prostorski kompromis za datoteke, kjer se iste oznake ponavljajo v tisočih vrsticah, pomeni pa, da mora bralnik to tabelo nizov naložiti vnaprej in jo ohraniti rezidentno, ker se katera koli celica v katerem koli listu sklicuje na kateri koli vnos v njej. Tabela je določena s številom ločenih nizov, ne s številom celic, zato ostaja skromna celo na ogromnih listih
Druga je preslikava formatov številk iz dela slogov. Numerična celica in celica z datumom sta bajt za bajtom enaki na žici: obe sta navadna številka, ker je datum v SpreadsheetML samo zaporedni štetec dni. Edina stvar, ki ju ločuje, je slog celice, ki kaže prek cellXfs v xl/styles.xml na ID formata številke. Da bralnik prikaže datum kot datum in ne kot surovo zaporedno številko, naloži to tabelo slog-v-format in jo ohrani rezidentno. Vse ostalo v datoteki -- dejanski podatki celic, ki predstavljajo večino bajtov -- pretoka mimo brez shranjevanja
Vsaka celica poroča vrsto in vrednost
Vsaka oddana celica prispe kot zapis TXLSDirectCell. Nosi indeks lista in ime, 1-osnovan indeks vrstice in stolpca, semantično Kind, Value kot Variant, besedilo Formula brez začetnega znaka enačaja in surovi StyleIndex. Vrsta je ena od xdkNumber, xdkString, xdkBoolean, xdkDate ali xdkError, tako da se lahko razvejate glede na pomen celice in ne ponovno izvajate atributov. Celica s formulo poroča vrsto svojega predpomnjega rezultata z besedilom formule poleg, tako da skupni seštevek prispe kot številka, ki vam pove tudi, kako je bil ustvarjen
type
TReportScan = class
procedure OnCell(Sender: TObject; const Cell: TXLSDirectCell;
var Abort: Boolean);
end;
procedure TReportScan.OnCell(Sender: TObject; const Cell: TXLSDirectCell;
var Abort: Boolean);
begin
case Cell.Kind of
xdkString: AccumulateLabel(Cell.Row, Cell.Col, VarToStr(Cell.Value));
xdkNumber: AddToTotals(Cell.Col, Double(Cell.Value));
xdkDate: NoteWhen(Cell.Row, VarToDateTime(Cell.Value));
xdkBoolean: FlagRow(Cell.Row, Boolean(Cell.Value));
xdkError: LogBadCell(Cell.Row, Cell.Col, VarToStr(Cell.Value));
end;
end;
Razlikovanje datuma od številke
Vprašanje datuma si zasluži podrobnejši pogled, ker je ravno to mesto, kjer večina naivnih skenerjev gre narobe. Na numerični celici ni vrste datuma. Celica, ki drži zaporedno vrednost 46000, je lahko količina, cena ali 17. februar 2025, in datoteka vam pove katero le prek ID-ja formata številke, doseženega prek sloga celice. ECMA-376 rezervira blok vgrajenih ID-jev formata, katerih pomen je fiksen pri vsakem skladnem proizvajalcu, in ID-ji, ki nosijo datume, sedijo v dveh razponih: 14 do 22 za standardne formate datuma in časa ter 45 do 47 za formate pretečenega časa, kot je [h]:mm:ss. Ko je DetectDates vklopljen, kar je privzeto, bralnik razreši slog vsake numerične celice na njen ID formata in celica, katere ID pade v te rezervirane razpone, je prijavljena kot xdkDate z njeno Value že pretvorjeno v Delphi TDateTime. Preverijo se tudi formati po meri z inšpekcijo kode formata za žetone datuma in časa, toda rezervirani razponi so zanesljiva hrbtenica. Izklopite DetectDates in tabela slogov sploh ni naložena -- vsaka numerična celica prispe kot xdkNumber in skeniranje je malce vitkejše
Preskoči liste in zgodaj prekini
Zaporedno skeniranje ima tiho prednost, ki je naključni dostop ne more ujemati: lahko se ustavite. Dogodek OnSheet se sproži pred odprtjem vsakega delovnega lista in vam daje dve stikali. Nastavite SkipSheet in ta cel del se nikoli ne razčleni -- tako skenirate le liste, ki vas zanimajo, v zvezku z več listi, ne da bi plačali za branje preostanka. Nastavite Abort in celotno skeniranje se takoj konča. Dogodek OnCell nosi lasten Abort, tako da se lahko ustavite takoj, ko ste našli tisto, kar ste iskali -- določeno vrstico, nadzorno vrednost, konec bloka glave -- ne da bi brali preostalih milijonov celic. Pri skeniranju samo naprej je prekinitev resnično brezplačna, ker je delo, ki ga preskočite, delo, ki se še ni zgodilo
procedure TReportScan.OnSheet(Sender: TObject; SheetIndex: Integer;
const SheetName: WideString; var SkipSheet: Boolean; var Abort: Boolean);
begin
// Scan only the "Data" sheet; leave the rest unread
SkipSheet := SheetName <> 'Data';
end;
Štetje celic brez upravljalca
Ena nedavna izboljšava je vredna omembe, ker pogosto zastavljeno vprašanje pretvori v en sam poceni klic. Bralnik šteje vsako poseljeno celico, ki jo preide, in to počne ne glede na to, ali je priložen upravljalec OnCell. Prej je brez nastavljenega upravljalca število poseljenih celic prišlo nazaj kot nič, ker je bilo štetje stranski učinek oddajanja. Zdaj je štetje neodvisno od oddajanja. To pomeni, da lahko postavite eno vprašanje -- koliko poseljenih celic dejansko vsebuje ta zvezek -- in dobite odgovor za ceno skeniranja brez povratnih klicev. ReadFile in ReadStream oba vrneta to skupno vrednost kot Int64, in enaka številka je potem na voljo kot lastnost CellCount. Vrednost -1 pomeni, da datoteke ni bilo mogoče odpreti ali da ni paket OOXML
var
Reader: TXLSDirectReader;
Populated: Int64;
begin
Reader := TXLSDirectReader.Create;
try
// No OnCell handler: a pure populated-cell census, still near-constant memory
Populated := Reader.ReadFile('quarterly_export.xlsx');
if Populated < 0 then
raise Exception.Create('Not a readable XLSX package')
else
Writeln(Format('%d populated cells (CellCount = %d)',
[Populated, Reader.CellCount]));
finally
Reader.Free;
end;
end;
Za polno skeniranje priložite upravljalec in pokličete ReadFile na popolnoma enak način. Kontrast s polnim nalaganjem je celoten namen: tam kjer bi nalaganje quarterly_export.xlsx v zvezek razširilo vsako celico v rezidentni objekt in obdržalo vse skupaj, neposredni bralnik ohranja le skupne nize in tabelo slogov, medtem ko dvanajst milijonov celic preteka skozi vaš OnCell po eno naenkrat. Aritmetika, ki se izvaja na celico, ne pusti ničesar za seboj, zato je vrhunski pomnilnik določen s številom ločenih nizov zvezka in ne z njegovo številom vrstic
Neposredni bralnik je pravo orodje, ko je naloga enkratno branje velikega zvezka in njegovo ekstrahiranje ali povzemanje. Ko namesto tega potrebujete naključni dostop polnega modela, vendar želite, da se obnaša na velikih datotekah, opombe o zmogljivosti velikih zvezkov v Delphiju pokrivajo to pot. Ko pa je smer obrnjena -- ustvarjanje velikega izhoda namesto porabe -- vodnik za pretočno pisanje za strežniška paketna opravila to isto disciplino konstantnega pomnilnika aplicira na pisanje. Vsi trije se dobavljajo kot del Komponente HotXLS za Delphi in C++Builder, skupaj z API-ji za branje, pisanje, formule in oblikovanje, obravnavanimi drugje na tem blogu