HotXLS читає джерела .xls, .xlsx, .xlsm, .ods, CSV і TSV через єдиний pull row cursor, TXLSRowCursor, чий FindFirst і FindNext просувають по одному логічному рядку за раз, поки в памʼяті лишається лише цей рядок. Машина станів на шість значень відділяє before-first від EOF, cancelled і faulted, а старіший callback-читач тепер є адаптером над тим самим курсором
Сценарій знайомий кожному, хто випускав функцію імпорту. Приходить .xlsx на 200 МБ, ви підключаєте обробник OnCell, а перша вимога після «прочитай це» звучить як «зупинись після першої сотні сторнованих проведень». Тепер форма вашого коду воює проти вас: цикл живе всередині бібліотеки, ваш обробник мусить піднімати прапорець, кожен наступний callback усе ще спрацьовує, доки парсер не помітить, а накопичений стан — скільки вже збігів, який стовпець збігся, що робити далі — мусить жити в полях класу, що існує лише щоб дати callback десь сидіти. Нічого з цього не є проблемою розбору. Це проблема потоку керування, і саме її прибирає pull-курсор
Скільки насправді коштує push callback на 200 МБ
Push вивертає керування навиворіт, і саме вивертання — те, чого викликач із фільтруванням чи зʼєднанням дозволити не може. З callback-API циклом володіє бібліотека, тож викликач не може використати Break, не може чергувати два джерела, не може передати читача підпрограмі, що очікує, щоб її рухали, і не може висловити «глянь на наступний рядок перед рішенням» без буферизації. Ціна не у пропускній здатності — добре написаний SAX callback-шлях стрімениться нормально — вона в тому, що кожен нетривіальний споживач вирощує власну маленьку машину станів, щоб імітувати цикл, який йому не дозволили написати. Помножте це на чотири файлові формати, кожен історично з власною точкою входу сканування, і семантика фільтрування, формул та помилок починає розповзатися між ними — це рівно той дрейф, який HotXLS мав на меті закрити
Як pull-курсор змінює ваш код виклику?
Він повертає цикл вам, а з ним і звичайний потік керування Pascal. TXLSRowCursor.Open приймає імʼя файла або TStream, визначає формат, одноразово завантажує спільні рядки та метадані стилів дат і вибирає аркуш 1. SelectSheet (з одиниці) або SelectSheetByName перенацілює на інший робочий аркуш і скидає курсор у before-first. 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 навантаження — регресійний набір доводить це формулами на 16 КіБ і кешованими рядками, які ніколи не матеріалізуються, коли їхній стовпець не спроєктовано. Ці опції свідомо заморожуються, поки прохід активний, і знову стають доступними для запису на EOF, при SelectSheet чи після Close, тож одне сканування ніколи не змішає два контракти декодування. Якщо вам потрібна лише опись аркушів, а не рядки, завантаження лише метаданих і вибіркових аркушів — дешевша точка входу
Один бекенд на формат, по одному циклу сканування кожному
Усередині HotXLS у кожного формату рівно один forward-сканер, і pull-курсор, і callback-читач керують тим самим сканером. 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?
Бо один boolean робить чотири різні ситуації нерозрізненними, і викликачі вгадують неправильно в усіх. TXLSRowCursorState називає їх явно
xrcsClosed— жодне джерело не відкритеxrcsBeforeFirst— відкрито або перенацілено, жодного рядка ще не прочитаноxrcsActive— стоїмо на валідному рядкуxrcsEof— аркуш спожито до кінцяxrcsCancelled— викликач свідомо зупинив прохідxrcsFaulted— прохід зазнав невдачі й початковий виняток було піднято
Останнє розрізнення — те, що має значення в продакшені. Відсутня частина аркуша чи невдалий старт проходу зберігає свій EReadError і переводить курсор у xrcsFaulted; його ніколи не понижують до простого False, який викликач прочитав би як «цей аркуш був порожній». Cancel свідомо вужчий за Close: він закриває бекенд поточного аркуша і його inflate-субпотік та інвалідує поточний рядок, але не вивільняє ZIP-архів і вихідний потік, а подвійний виклик — no-op. Після скасування ви відновлюєтеся явним викликом SelectSheet — курсор не перезапустить прохід мовчки за вас. Володіння потоком слідує тому ж оборонному правилу: xsoBorrowed — типовий випадок і відновлює позицію потоку при закритті, xsoOwned передає володіння лише після того, як Open уже встиг успішно завершитися, тож невдалий 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 forward тримають
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 існує, щоб продемонструвати: памʼять слідкує за шириною рядка, а не за кількістю рядків. Він акумулює записи клітинок, Variants, рядки формул і rich-text навантаження поточного вихідного рядка та вбирає специфічний для формату робочий набір — логічний запис CSV, фізичний шаблон рядка ODS, пік записів BIFF або XLSX-клітинку, яку зараз декодують. Читайте його разом із SheetPassesStarted, що рахує, скільки проходів аркушів справді почалися. Два застереження тримають це чесним: число — оцінка, а не точний облік купи, і воно монотонне від останнього Open, тож це інструмент налагодження та регресії, а не живий датчик. Про ширшу картину того, куди йдуть час і байти на дуже великих книгах, дивіться продуктивність великих книг у Delphi
Push-читач став адаптером — і чого курсор не робитиме
TXLSForwardReader більше не несе окремих точок входу сканування XLSX, BIFF і тексту. Він налаштовує курсор, проходить його і перекладає поточний рядок у події OnSheet та OnCell, саме тому дві фасади більше не можуть розійтися у фільтруванні, стані формул чи обробці помилок. Два наслідки варто знати до оновлення: callback SheetIndex тепер скрізь на TXLSForwardReader нумерується з одиниці (TXLSDirectReader зберігає свій наявний контракт подій з нуля), а OnSheet спрацьовує до SelectSheet, тож установка SkipSheet означає, що частина аркуша взагалі ніколи не відкривається й не розпаковується. Межі так само явні: книгу не можна модифікувати, поки прохід активний, скасування вимагає явного перезапуску, а BIFF forward-шлях ніколи не декомпілює токени формул, тож класичні клітинки з формулами звітують HasFormula true при FormulaTextAvailable false і віддають кешований результат замість вигадувати порожній рядок формули. Row cursor і його адаптер пройшли 1 298 перевірок на Delphi Win32 і Win64 плюс статичний пакет C++Builder 37.0 Win64
Якщо ви зважуєте pull-курсор проти завантажувача, який маєте зараз, питання не в тому, котрий парсить швидше, а в тому, котрий дозволяє вам написати умову виходу, яка вам справді потрібна. Повні деталі компонента, підтримувані версії IDE та ліцензування — на сторінці компонента електронних таблиць HotXLS для Delphi