Техническа статия

Pull row курсор за XLS, XLSX, ODS и CSV в Delphi

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 като оставащ брой, а не разширяване, и напредва покрити клетки, без да излъчва стойности. Стрийминг директният четец споделя същия зареждач на споделени низове и стилове за дати

Издърпващият курсор за редове на HotXLS се разпределя към по един напреднал скенер на формат — SAX бекенд за XLSX, парсер на записи за BIFF, текстов бекенд, подушващ разделители, и шаблон на ред за ODS — с четеца с обратно извикване конфигуриран отгоре като адаптер
Всеки формат има точно един напреднал скенер, и както издърпващият курсор, така и четецът с обратно извикване карат същия скенер, така че семантиката на филтриране и грешки не може да се размине

Защо шест състояния вместо един Eof флаг?

Защото един-единствен булев прави четири различни ситуации неразличими, а извикващите грешат за всички тях. TXLSRowCursorState ги назовава изрично

  • xrcsClosed — няма отворен източник
  • xrcsBeforeFirst — отворен или пренасочен, още няма прочетен ред
  • xrcsActive — стои на валиден ред
  • xrcsEof — листът е изяден докрай
  • xrcsCancelled — извикващият спря прохода съзнателно
  • xrcsFaulted — проходът се провали и оригиналното изключение бе вдигнато

Това последно разграничение е това, което има значение в продукция. Липсваща част от работен лист или провален старт на проход запазва своя EReadError и премества курсора в xrcsFaulted; никога не се деградира до обикновено False, което извикващ би прочел като „този лист беше празен“. Cancel е умишлено по-тесен от Close: той затваря текущия бекенд на работния лист и неговия inflate подпоток и инвалидидира текущия ред, но не освобождава ZIP архива или изходния поток, а извикването му два пъти е no-op. След отказ възстановявате, като извикате SelectSheet изрично — курсорът няма тихо да рестартира проход вместо вас. Притежанието на потока следва същото защитно правило: xsoBorrowed е по подразбиране и възстановява позицията на потока при затваряне, xsoOwned прехвърля притежанието едва след като Open вече е успял, така че провален отвор никога не освобождава поток, който извикващият още държи

Шестте състояния на курсора за редове на HotXLS с преходите между тях, показващи как Cancel премества активен проход в отменен, провален старт на проход го премества в провален, и как и двете остават различими от край на лист
Шест именувани състояния държат празен лист, съзнателно спиране и провален проход разграничими, което един-единствен Eof булев не може да направи
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

Сравнение в HotXLS, показващо зареждане на цял лист, държащо всички резидентни редове, срещу издърпващия курсор, държащ само текущия ред плюс един работен набор на формата — точно това натрупва и докладва PeakRowBufferedBytes
PeakRowBufferedBytes натрупва текущия изходен ред плюс специфичния за формата работен набор, така че паметта следва колко е широк един ред, а не колко реда има листът

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 компонента за електронни таблици