Et regneark med en million rader og et dusin kolonner er en helt vanlig eksport fra en database-rapporteringsjobb. Åpne det på vanlig måte, ved å laste hele arbeidsboken inn i en TXLSWorkbook, og prosessen må materialisere hver og en av de tolv millioner cellene som et levende objekt før din første linje med forretningslogikk kjører. Filen på disken kan være seksti megabyte komprimert XML. Objekttreet den utvides til er flere ganger det, og alt må være residerende på en gang fordi modellen er tilfeldig tilgang av design. For en rapport du har tenkt å lese fra topp til bunn og kaste, er det svært mye minne brukt på en struktur du aldri trengte
Det er en annen vei gjennom den samme filen. I stedet for å bygge en modell, skanner du regneark-XML-en bare fremover, én celle om gangen, og lar hver celle flyte forbi etter at du har sett på den. Ingenting akkumuleres. Minnet forblir tilnærmet konstant enten arket har tusen rader eller ti millioner, fordi leseren aldri holder mer enn den delen den for øyeblikket analyserer pluss et par små oppslagstabeller. Dette er hva HotXLS-direkteleseren gjør, og resten av denne artikkelen handler om hvorfor den holder seg liten og hva den gir deg i bytte
Hvorfor minnemodellen ikke skalerer
En XLSX-fil er en ZIP-pakke med XML-deler beskrevet av ECMA-376. Hvert regneark er sin egen del, xl/worksheets/sheetN.xml, og inni den er hver rad et <row>-element som inneholder <c>-celleelementer. Den vanlige lastebanen leser den delen og konstruerer et adresserbart objekt for hver celle slik at du senere kan be om Cells[12345, 7] og få et svar på konstant tid. Tilfeldig tilgang er hele poenget med en arbeidsbokmodell, og det er nøyaktig det som gjør redigering, formelevaluering og stilsetting praktisk
Kostnaden er at tilfeldig tilgang krever at alt er til stede samtidig. Du kan ikke indeksere inn i en struktur du bare delvis har bygget. Så det maksimale minnet for en full innlasting er en funksjon av antall celler, og på et ark med millioner av fylte celler lander den funksjonen et sted tjenesten din ikke ønsker å være, spesielt hvis flere slike jobber kjører samtidig på en delt maskin. Når tilgangsmønsteret du faktisk trenger er sekvensielt, er det å betale for tilfeldig tilgang det samme som å betale for en funksjon du ikke vil bruke
En kun-fremover SAX-skanning som ikke bygger noe tre
Direkteleseren åpner ZIP-pakken og går gjennom hver regnearkdel med en SAX-stil pull-parser. SAX betyr her at parseren rapporterer analysehendelser etter hvert som den støter på dem, et startelement, en tekstkjøring, et sluttelement, og går deretter videre. Den beholder ikke noe nodetre bak seg. Leseren sporer gjeldende rad og kolonne fra r-attributtene, samler inn cellens type, stilindeks, verdi og formeltekst etter hvert som hendelsene ankommer, og når den lukkende </c>-taggen sees, sender den ut én celle og glemmer den. Den neste cellen gjenbruker den samme håndfullen med lokale variabler
Fordi ingenting beholdes mellom celler, vokser ikke minneavtrykket med antall celler. Det er egenskapen det er verdt å holde fast ved. Et ark med to hundre rader og et ark med tjue millioner rader koster leseren det samme i residerende minne, og forskjellen mellom dem er bare hvor lenge skanningen varer. Du gir opp tilfeldig tilgang, modellens hovedfunksjon, og til gjengjeld får du et tak på minnet som celleantallet ikke kan presse seg gjennom
Hva som forblir residerende, og hvorfor disse to delene
Skanningen er ikke helt tilstandsløs, og unntakene er lærerike. To små tabeller må holdes i minnet så lenge skanningen pågår, fordi en celle på egen hånd ikke bærer nok informasjon til å tolkes uten dem
Den første er den delte strengtabellen. I SpreadsheetML lagrer ikke en tekstcelle sin egen tekst. Den bærer t="s" og en numerisk nyttelast som er en indeks til xl/sharedStrings.xml, en enkelt deduplikert liste over hver distinkt streng i arbeidsboken. Dette er en god plassutveksling for filer der de samme etikettene gjentas over tusenvis av rader, men det betyr at leseren må laste inn strengtabellen på forhånd og holde den residerende, fordi enhver celle hvor som helst i ethvert ark kan referere til en hvilken som helst oppføring i den. Tabellstørrelsen bestemmes av antall distinkte strenger, ikke av celleantallet, så den forblir beskjeden selv på enorme ark
Den andre er nummerformattilordningen fra stildelen. En numerisk celle og en datocelle er byte-for-byte det samme på tråden: begge er et vanlig tall, fordi en dato i SpreadsheetML bare er en seriell dagtelling. Det eneste som skiller dem er cellens stil, som peker gjennom cellXfs i xl/styles.xml til en nummerformat-ID. For å rapportere en dato som en dato snarere enn som det rå serielle nummeret, laster leseren den stil-til-format-tabellen og holder den residerende. Alt annet i filen, de faktiske celledataene som utgjør mesteparten av bytene, strømmer forbi uten å bli lagret
Hver celle rapporterer en type og en verdi
Hver celle som sendes ut kommer som en TXLSDirectCell-post. Den bærer arkets indeks og navn, den 1-baserte raden og kolonnen, en semantisk Kind, Value som en Variant, Formula-teksten uten det ledende likhetstegnet, og den rå StyleIndex. Typen er en av xdkNumber, xdkString, xdkBoolean, xdkDate eller xdkError, slik at du kan forgrene basert på hva cellen betyr snarere enn å utlede det på nytt fra attributter. En formelcelle rapporterer typen til det bufrede resultatet sitt, sammen med formelteksten, så en beregnet total kommer gjennom som et tall som også forteller deg hvordan det ble produsert
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;
Skille en dato fra et tall
Datospørsmålet fortjener en nærmere titt fordi det er der de fleste naive skannere gjør feil. Det er ingen datotype på en numerisk celle. En celle som holder den serielle verdien 46000 kan være en mengde, en pris, eller den 17. februar 2025, og filen forteller deg hvilken bare gjennom nummerformat-ID-en som nås via cellens stil. ECMA-376 reserverer en blokk med innebygde format-ID-er hvis mening er fast på tvers av hver samsvarende produsent, og de datobærende ID-ene sitter i to områder: 14 til 22 for standard dato- og tidsformater, og 45 til 47 for medgått-tid-formater som [h]:mm:ss. Når DetectDates er på, noe det er som standard, løser leseren hver numerisk celles stil til dens format-ID, og en celle hvis ID faller i disse reserverte områdene rapporteres som xdkDate med dens Value allerede konvertert til en Delphi TDateTime. Egendefinerte formater sjekkes også, ved å inspisere formatkoden for dato- og tidstokener, men de reserverte områdene er den pålitelige ryggraden. Slå av DetectDates, og stiltabellen lastes ikke engang inn, hver numeriske celle kommer gjennom som xdkNumber, og skanningen er brøkdelen slankere
Hopp over ark og avbryt tidlig
Sekvensiell skanning har en stille fordel som tilfeldig tilgang ikke kan matche: du kan stoppe. OnSheet-hendelsen utløses før hvert regneark åpnes, og gir deg to brytere. Sett SkipSheet og hele den delen analyseres aldri, som er måten du skanner bare de arkene du bryr deg om i en flerarks arbeidsbok uten å betale for å lese resten. Sett Abort og hele skanningen avsluttes umiddelbart. OnCell-hendelsen bærer sin egen Abort, så du kan stoppe i det øyeblikket du har funnet det du lette etter, en bestemt rad, en vaktverdi, slutten på en overskriftsblokk, uten å lese de resterende millioner av cellene. På en kun-fremover skanning er avbrytelse genuint gratis, fordi arbeidet du hopper over er arbeid som ennå ikke hadde skjedd
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;
Telle celler uten en hendelseshåndterer
En nylig forbedring er verdt å trekke frem fordi den gjør et vanlig spørsmål til et enkelt billig kall. Leseren teller hver fylt celle den passerer, og den gjør dette uavhengig av om en OnCell-håndterer er tilknyttet eller ikke. Tidligere, uten noen håndterer satt, kom tellingen for fylte celler tilbake som null, siden telling var en bieffekt av utsending. Nå er tellingen uavhengig av utsending. Det betyr at du kan stille ett spørsmål, hvor mange fylte celler inneholder denne arbeidsboken faktisk, og få svaret for prisen av en skanning helt uten tilbakeringinger. Både ReadFile og ReadStream returnerer den totalen som en Int64, og det samme tallet er tilgjengelig etterpå som egenskapen CellCount. Et retursvar på -1 signaliserer at filen ikke kunne åpnes eller ikke er en OOXML-pakke
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;
For hele skanningen kobler du til håndtereren og kaller ReadFile på nøyaktig samme måte. Kontrasten til en full innlasting er hele poenget: der lasting av quarterly_export.xlsx inn i en arbeidsbok ville utvide hver celle til et residerende objekt og holde på alt, beholder direkteleseren bare de delte strengene og stiltabellen mens de tolv millioner cellene flyter gjennom din OnCell én om gangen. Aritmetikken som kjørte per celle legger ingenting igjen, så minnetoppen settes av arbeidsbokens antall distinkte strenger, ikke av antallet rader
Direkteleseren er det riktige verktøyet når jobben er å lese en stor arbeidsbok én gang og trekke ut eller oppsummere den. Når du i stedet trenger tilfeldig tilgang fra den fulle modellen, men vil at den skal oppføre seg pent på store filer, dekker justeringen i våre notater om ytelse for store arbeidsbøker i Delphi den veien. Og når retningen er omvendt, og produserer store utdata i stedet for å konsumere det, anvender gjennomgangen for strømmende skriving for serverbatchjobber den samme disiplinen med konstant minne på skriving. Alle tre leveres som en del av HotXLS-komponenten for Delphi og C++Builder, sammen med API-ene for lesing, skriving, formler og formatering som dekkes andre steder på denne bloggen