HotXLS чете .xls, .xlsx, .xlsm, .ods, CSV и TSV източници през един-единствен издърпващ курсор за редове, TXLSRowCursor, чийто FindFirst и FindNext напредват с един логически ред наведнъж, докато само този ред остава в паметта. Машина на състоянията с шест стойности отделя преди-първия от EOF, отменено и провалено, а по-старият четец с обратно извикване вече е адаптер върху същия курсор
Сценарият е познат на всеки, който е пуснал функция за импорт. Пристига .xlsx от 200 MB, вие закачате OnCell handler, а първото изискване след „прочети го“ е „спри след първите сто обърнати постъпления“. Сега формата на кода ви се бори с вас: цикълът живее вътре в библиотеката, handler-ът ви трябва да вдигне флаг, всяко следващо обратно извикване пак се задейства, докато парсерът не забележи, а натрупаното състояние — колко попадения досега, коя колона е съвпаднала, какво следва — трябва да живее в полета на клас, който съществува само за да даде на обратно извикването къде да седне. Нищо в това не е проблем на парсването. Това е проблем на управлението на потока, и точно той е този, който един издърпващ курсор премахва
Какво реално струва едно обратно извикване тип push при 200 MB
Push обръща управлението, а обръщането е точно това, което извикващ, който филтрира или съединява, не може да си позволи. При callback API библиотеката притежава цикъла, така че извикващият не може да използва Break, не може да преплита два източника, не може да предаде четеца на рутина, която очаква да бъде управлявана, и не може да изрази „погледни следващия ред, преди да решиш“ без буфериране. Цената не е пропускателната способност — добре написан SAX callback път стриймва без проблем — а това, че всеки нетривиален потребител отглежда собствена малка машина на състоянията, за да симулира цикъла, който не е имал право да напише. Умножете това по четири файлови формата, всеки исторически със своя входна точка за сканиране, и семантиките за филтриране, формули и грешки започват да се разминават помежду си — точно дрейфът, който HotXLS си нареди да затвори
Как един издърпващ курсор променя извикващия ви код?
Той връща цикъла на вас, а с него и обикновеното Pascal управление на потока. TXLSRowCursor.Open приема име на файл или TStream, разпознава формата, зарежда споделените низове и метаданните за стиловете за дати веднъж и избира лист 1. SelectSheet (базиран на едно) или SelectSheetByName пренасочва към друг работен лист и нулира курсора към преди-първия. FindFirst и FindNext след това се позиционират на следващия населения ред — редовете без декодируеми клетки се прескачат, така че RowIndex може да скочи — а текущият ред е изложен като CellCount, Cells[] и ValueByCol[], всички базирани на едно по оста на колоните. Излизането от цикъла е едно Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // прескача заглавната зона
Cursor.IncludeColumn(1); // декодира само тези две колони
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; // обикновен Break; без флаг за отказ, без сентинел
end;
until not Cursor.FindNext;
finally
Cursor.Free; // деструкторът приключва прохода
end;
end;
Проекцията и диапазонът се задават преди прохода, а не се филтрират след него. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates и DetectTextTypes всички се зачитат вътре в бекендовете, така че неизбрана колона изобщо не заделя своята стойност, формулен низ или rich-text payload — регресионният пакет доказва това с формули от 16 KiB и кеширани низове, които никога не се материализират, когато колоната им не е проектирана. Тези настройки са умишлено замразени, докато проход е активен, и стават записваеми отново при EOF, при SelectSheet или след Close, така че едно сканиране никога не може да смеси два договора за декодиране. Ако ви трябва само инвентарът на листовете, а не редовете, зареждане само на метаданни и селективно на листове е по-евтината входна точка
По един бекенд на формат, по един цикъл на сканиране
Всеки формат има точно един напреднал скенер вътре в HotXLS, и както издърпващият курсор, така и четецът с обратно извикване карат един и същ скенер. TXLSXForwardRowBackend е единствената машина на състоянията за SAX на работни листове за части от ECMA-376 Part 1 §18.3, държаща XML четеца, таблицата със споделени формули и rich-text парсера, и тя напредва до точно една физическа граница <row> на извикване. TXLSBiffForwardParser притежава глобалните променливи, избора на лист и напредването по редове за потока от записи на [MS-XLS]; направата му паузируем роди най-острото ограничение в целия дизайн, защото кеширана низова формула е запис Formula, веднага последван от запис String, така че точка на прекъсване на ниво ред никога не трябва да попада между двата. TXLSForwardTextBackend държи четец, съзнаващ BOM, активния разделител и един логически запис — CSV подушва запетая, точка и запетая, табулация или вертикална черта от първия запис, като игнорира цитирани символи, а многоредовите цитирани полета се слепват с #10, така че номерът на ред следва логическите записи, а не физическите нови редове. TXLSForwardOdsBackend държи един-единствен физически шаблон на ред за таблици по OpenDocument §9, третира table:number-rows-repeated като оставащ брой, а не разширяване, и напредва покрити клетки, без да излъчва стойности. Стрийминг директният четец споделя същия зареждач на споделени низове и стилове за дати
Защо шест състояния вместо един Eof флаг?
Защото един-единствен булев прави четири различни ситуации неразличими, а извикващите грешат за всички тях. TXLSRowCursorState ги назовава изрично
xrcsClosed— няма отворен източникxrcsBeforeFirst— отворен или пренасочен, още няма прочетен редxrcsActive— стои на валиден редxrcsEof— листът е изяден докрайxrcsCancelled— извикващият спря прохода съзнателноxrcsFaulted— проходът се провали и оригиналното изключение бе вдигнато
Това последно разграничение е това, което има значение в продукция. Липсваща част от работен лист или провален старт на проход запазва своя EReadError и премества курсора в xrcsFaulted; никога не се деградира до обикновено False, което извикващ би прочел като „този лист беше празен“. Cancel е умишлено по-тесен от Close: той затваря текущия бекенд на работния лист и неговия inflate подпоток и инвалидидира текущия ред, но не освобождава ZIP архива или изходния поток, а извикването му два пъти е no-op. След отказ възстановявате, като извикате SelectSheet изрично — курсорът няма тихо да рестартира проход вместо вас. Притежанието на потока следва същото защитно правило: xsoBorrowed е по подразбиране и възстановява позицията на потока при затваряне, xsoOwned прехвърля притежанието едва след като Open вече е успял, така че провален отвор никога не освобождава поток, който извикващият още държи
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: курсорът никога не освобождава Src, а Close възстановява
// позицията, която потокът е имал при извикването на Open
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // затваря само бекенда на работния лист и неговия
Break; // inflate подпоток; идемпотентен
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; // все още наш, все още валиден, позицията е възстановена
end;
end;
Заемане на текущия ред без копиране
IXLSRowCursorView предава ред на друга рутина без да дублира масива от клетки. Изгледът съхранява споделена защита, държаща указателя към курсора плюс брояч на поколения UInt64; напредването, изборът на лист, отказът, затварянето и унищожаването на курсора всички увеличават това поколение, а унищожаването допълнително изчиства собственика на защитата. Така остарял изглед не може да прочете освободена памет: Valid е сонда без изключение, която можете да извикате по всяко време, докато всеки друг член първо валидира и вдига EXLSRowCursorViewInvalidated. Бъдете честни за това какъв е този договор — това е fail-fast за времето на живот, а не гаранция за нишкова безопасност, и не дава право да четете ред от втора нишка, докато първата напредва курсора
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // заема; масивът от клетки не се копира
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 напредналите четения пазят
else if Cell.Kind = xdkEmpty then // кеширания резултат, не токените
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank са реални клетки
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// Интерфейсът надживява цикъла, но редът зад него — не
if not View.Valid then // Valid никога не вдига изключение; Cells[] сега би вдигнал
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes и какво има право да докаже
PeakRowBufferedBytes съществува, за да демонстрира, че паметта следва ширината на реда, а не броя на редовете. Той натрупва записите на клетки, Variant-ите, формулните низове и rich-text payload-ите на текущия изходен ред и сгъва в тях специфичния за формата работен набор — логическия запис на CSV, физическия шаблон на ред за ODS, пика на BIFF записи или суровата XLSX клетка, която в момента се декодира. Четете го заедно с SheetPassesStarted, който брои колко прохода по работни листове реално са започнали. Две уговорки го държат честен: цифрата е оценка, а не точно отчитане на heap, и е монотонна от най-скорошното Open, така че е инструмент за дебъгване и регресия, а не жив измервателен уред. За по-широката картина къде отиват времето и байтовете при много големи книги вижте производителността на големи работни книги в Delphi
Push четецът стана адаптер — и какво курсорът няма да направи
TXLSForwardReader вече не носи отделни входни точки за сканиране на XLSX, BIFF и текст. Той конфигурира курсор, обхожда го и превежда текущия ред в събития OnSheet и OnCell, което е причината двете фасади вече да не могат да се разминат по филтриране, състояние на формули или обработка на грешки. Две последствия си струва да знаете, преди да надстроите: callback SheetIndex вече е еднакво базиран на едно в TXLSForwardReader (TXLSDirectReader запазва съществуващия си договор за събития, базиран на нула), а OnSheet се задейства преди SelectSheet, така че задаването на SkipSheet означава, че частта от работния лист изобщо не се отваря и не се декомпресира. Границите са еднакво изрични: работната книга не бива да се изменя, докато проход е активен, отказът изисква изричен рестарт, а BIFF напредналият път никога не декомпилира формулни токени, така че класическите формулни клетки докладват HasFormula истина с FormulaTextAvailable лъжа и ви подават кеширания резултат, вместо да измислят празен формулен низ. Курсорът за редове и неговият адаптер минаха 1,298 проверки на Delphi Win32 и Win64 плюс статическия пакет на C++Builder 37.0 Win64
Ако претегляте издърпващ курсор срещу зареждача, който имате сега, въпросът не е кой парсва по-бързо, а кой ви позволява да напишете условието за изход, което реално ви трябва. Пълни детайли за компонента, поддържаните версии на IDE и лицензирането са на страницата на HotXLS Delphi компонента за електронни таблици