Задачата за отчитане работи добре цяла година. Тя изгражда работна книга, попълва лист с това, което заявката връща, и го записва. След това клиент с петгодишна история иска пълен експорт, броят на редовете надхвърля милион и процесът умира с грешка за недостиг на памет много преди файлът да достигне до диска. Нищо не е било грешно в кода. Той е задържал цялата работна книга в RAM, за да може да я сериализира накрая, и паметта, от която се е нуждаел, е нараствала успоредно с броя на редовете, които е трябвало да запише
Решението не е по-голяма машина. То е различен модел на писане. Стрийминг директният писател в HotXLS излъчва OOXML пакета постепенно с пристигането на редовете, така че паметта, която използва, не зависи от това колко реда записвате. Това е еквивалентът от страна на писането на стрийминг четеца: там, където четецът обхожда огромен лист без да изгражда дърво от клетки, писателят създава такова също без да изгражда дърво от клетки
Защо нормалният път за запис нараства с данните
Редовният път с TXLSXWorkbook първо изгражда пълен обектен модел. Всяка клетка, със своята стойност, тип и препратка към стил, живее като обект в паметта, докато не извикате запис, в който момент цялото дърво се сериализира в пакета. Този модел е правилният, когато искате да прочетете лист, да го редактирате, да преизчислите и да го запишете обратно, защото случайният достъп до която и да е клетка е точно това, от което се нуждае редактирането. Това е грешният модел, когато изливате редове в една посока и никога не се връщате назад, защото плащате за това да поддържате всеки ред резидентен без никаква полза. Милион реда от обекти са милион реда от обекти, независимо дали някога се връщате към тях или не
Стрийминг писателят премахва дървото. Веднага след като дадена клетка бъде записана, тя се превръща в байтове в частта на работния лист и тези байтове се предават към zip изхода. Потокът на работния лист е единственият буфер, който расте, и той расте от страна на изхода, а не като активни Delphi обекти в хийпа. Това, което остава резидентно, е фиксирано количество счетоводство: имената на листовете, няколко флага, номерът на текущия ред, брояч на клетки. Този набор не се променя между ред едно и ред десет милиона
Таблицата със споделени низове е капанът, а инлайн низовете са изходът
Повечето стрийминг XLSX писатели се справят добре, докато не се сблъскат с текст. Форматът OOXML обикновено съхранява низове в таблица със споделени низове: всеки отделен низ се записва веднъж в отделна част и всяка клетка, която съдържа този низ, носи индекс към таблицата вместо текста. Това е добра оптимизация на пространството за файлове, пълни с повтарящи се етикети, и това е настройката по подразбиране, която използва стандартният път за запазване. Проблемът за стрийминг писателя е брутален. За да се премахнат дублиранията, таблицата трябва да остане резидентна за цялата задача, защото всеки ред, който предстои, може да повтори низ от вече записан ред, и само пълна карта в паметта на видените низове може да присвои правилния индекс. Така че единствената структура, която стрийминг писателят не може да стриймва, е самата структура, която трябва да направи файла малък. Данните с много текст побеждават стрийминга, за който сте дошли
Директният писател заобикаля таблицата изцяло. Низовете се записват инлайн, като t="inlineStr" клетки, чийто текст седи директно вътре в клетката с елемент <is><t>. Няма таблица за натрупване и няма карта на видените низове за задържане, така че текстовите колони не струват повече памет от числовите. Компромисът е ясен и си струва да се заяви ясно. Инлайн низовете повтарят същия текст, където и да се среща, така че файл с много идентични етикети е по-голям на диск от еквивалента със споделени низове. Вие харчите размер на файла, за да купите постоянна памет. За еднократно експортиране това е правилната страна на компромиса и zip компресията абсорбира голяма част от повторението на изхода така или иначе
Таблицата със стилове пристига накрая, с един формат за дата
Стиловете представят същото напрежение като низовете. Работната книга препраща към своето форматиране чрез част със стилове, и стрийминг писателят не може да поддържа нарастваща палитра от стилове в крачка с клетките, които вече е изпратил. Директният писател отговаря на това, като поддържа таблицата със стилове малка и фиксирана и я излъчва при затваряне, а не предварително. Един формат на клетка по подразбиране покрива обикновените клетки. Един цифров формат за дата покрива датите, регистриран с код на формат yyyy-mm-dd на известна позиция в списъка с формати на клетки
Този формат за дата е причината WriteDateTime да съществува като свое собствено извикване. Excel няма собствен тип дата; датата е число, носещо формат за дата. WriteDateTime записва стойността като обикновен сериен номер и маркира клетката с единствения стил за дата, така че електронната таблица я изобразява като дата, а не като петцифрено цяло число. Серийният номер, който записва, има значение за двупосочната съвместимост. Той съхранява стойността TDateTime директно под системата за дати 1900, което е същата конвенция, която използва обичайният път за запазване на TXLSXWorkbook. Тъй като и двата пътя са съгласни относно серийния номер, файлът, произведен от стрийминг писателя, се чете обратно през четеца на HotXLS и се отваря в Excel с дати, които съвпадат с това, което сте възнамерявали, без изненади с разминаване с единица или епоха между писателя и четеца
Редът е задължителен, защото байтовете вече са изчезнали
Стриймингът купува своя профил на паметта с едно правило, което трябва да спазвате. Изходът се излъчва в движение и не може да бъде посетен отново, така че всичко трябва да бъде записано в реда, в който се появява във файла. В рамките на един ред клетките вървят във възходящ ред на колоните. В рамките на един лист редовете вървят във възходящ ред. Няма буфер, който да позволява на писателя да сортира вашите клетки постфактум, защото редът, който току-що сте затворили, вече са байтове в zip потока и вече не са достъпни. Подайте му колона 5 и след това колона 2 в същия ред и изходът е неправилно формиран, тъй като писателят просто излъчва това, което му давате, в последователността, в която му го давате
API-то за редове има малко удобство за често срещания случай. AddRow приема 1-базиран индекс на ред, но подаването на 0 означава да вземете следващия ред след предишния, така че последователното запълване не трябва да проследява и предава нарастващ брояч. Всяко AddRow затваря реда преди него, и всяко AddSheet затваря листа преди него, така че никога не завършвате изрично ред или лист. Започвате следващия и писателят финализира отворената структура вместо вас
Екранирането се обработва там, където текстът влиза в XML
Всеки текст, който записвате, става част от XML документ, така че петте предварително дефинирани XML обекта трябва да бъдат екранирани или пакетът е невалиден в момента, в който дадена стойност съдържа амперсанд или ъглова скоба. Писателят екранира &, <, >, " и ' вместо вас както в инлайн текста на низа, така и в текста на формулата, двете места, където предоставени от извикващия символи попадат вътре в маркирането. Вие подавате суров WideString и писателят го прави безопасен. Име на продукт като Smith & Co <Ltd> или формула, препращаща към име на лист в кавички, излиза като добре формиран XML без никакво екраниране от ваша страна
Жизнен цикъл и защо Destroy все още затваря
Завършването на пакета е това, което записва частта на работната книга, частта със стилове, частите с типовете съдържание и връзките, и накрая централната директория на zip. Тази работа се случва в Close. Пакет, който никога не е затворен, е непълен zip, който нито една програма за електронни таблици няма да отвори, така че затварянето не е незадължително почистване, то е стъпката, която прави файла валиден. За да се предпази от забравено Close в път на грешка, Destroy извършва най-доброто възможно затваряне, ако пакетът все още е отворен, така че освобождаването на писателя не води до изтичане на основния zip обект, дори когато изключение е пропуснало изричното извикване. Надеждният модел все още е обичайният за Delphi: пишете вътре в try, извиквате Close и освобождавате в finally
Стрийминг на голям лист от край до край
Формата на задачата е: начало, добавяне на лист, изливане на редове, затваряне. Примерът по-долу записва хедърен ред и след това дълга поредица от редове с типизирани данни, смесвайки низове, числа, формула без кеширан резултат и дата. Паметта, която използва за десет реда и за десет милиона реда, е една и съща, защото всяка клетка заминава за zip потока веднага щом бъде записана
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;
Втори лист е просто още едно AddSheet преди да продължите, и писателят затваря първия лист, докато отваря втория. Булевите флагове използват WriteBoolean, което записва типизирана булева клетка, а не текста "True". Ако искате да потвърдите, че файлът е здрав и се чете двупосочно, свойството CellCount отчита колко клетки са записани, и обратното четене на резултата със стрийминг четеца трябва да отчете същата обща сума
// 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]));
Писането в поток вместо във файл е същият код с BeginStream на мястото на BeginFile, което позволява на сървър да изпрати работната книга към HTTP отговор или поток в паметта без временен файл на диска. Писателят не притежава потока, който предавате, така че вие запазвате контрола върху неговия живот
Когато работата е сървърна крайна точка, която изгражда работни книги при поискване, моделите в стрийминг писане за сървърни партидни задачи показват как да свържете това в манипулатор на заявки и планирано експортиране. Когато въпросът е по-широката цена на много големи работни книги, както четене, така и писане, производителността на големи работни книги в Delphi покрива къде всъщност отиват времето и паметта. Стрийминг директният писател се доставя като част от компонента HotXLS за Delphi и C++Builder, заедно с пълните API-та за четене, редактиране и запис, разгледани на други места в този блог