HotXLS czyta źródła .xls, .xlsx, .xlsm, .ods, CSV i TSV przez pojedynczy kursor wierszy pull, TXLSRowCursor, którego FindFirst i FindNext przesuwają o jedną logiczną literę wiersza naraz, podczas gdy tylko ten wiersz zostaje w pamięci. Sześciowartościowa maszyna stanów oddziela przed-pierwszym od EOF, anulowanego i zawiedzionego, a starszy czytnik callbackowy jest teraz adapterem nad tym samym kursorem
Scenariusz jest znajomy każdemu, kto wysłał kiedyś funkcję importu. Przychodzi .xlsx na 200 MB, podpinasz obsługę OnCell, a pierwsze wymaganie po „przeczytaj to” brzmi „zatrzymaj się po pierwszej setce księgowań REVERSED”. Teraz kształt twojego kodu walczy z tobą: pętla mieszka w bibliotece, twoja obsługa musi podnieść flagę, każde kolejne wywołanie zwrotne i tak strzela, aż parser zauważy, a zakumulowany stan — ile trafień do tej pory, która kolumna pasowała, co robić dalej — musi mieszkać w polach klasy, która istnieje tylko po to, by dać callbackowi gdzie usiąść. Nic z tego nie jest problemem parsowania. To problem przepływu sterowania, i to właśnie ten, który kursor pull usuwa
Co naprawdę kosztuje callback push przy 200 MB
Push odwraca sterowanie, a odwrócenie jest dokładnie tym, na co nie może sobie pozwolić wywołujący filtrujący albo złączający. Przy API callbackowym biblioteka jest właścicielem pętli, więc wywołujący nie może użyć Break, nie może przeplatać dwóch źródeł, nie może oddać czytnika procedurze, która oczekuje być napędzana, i nie może wyrazić „zajrzyj do następnego wiersza, zanim zdecydujesz” bez buforowania. Kosztem nie jest przepustowość — dobrze napisana ścieżka callbackowa SAX strumieniuje dobrze — jest nim to, że każdy nietrywialny konsument wyhodowuje własną małą maszynę stanów, by symulować pętlę, którą nie wolno mu było napisać. Pomnóż to przez cztery formaty plików, każdy historycznie z własnym punktem wejścia skanowania, i semantyka filtrowania, formuł i błędów zaczyna się rozjeżdżać między nimi, co jest dokładnie tym dryfem, który HotXLS wyszedł zamknąć
Jak kursor pull zmienia twój kod wywołujący?
Oddaje ci pętlę, a z nią zwykły przepływ sterowania Pascala. TXLSRowCursor.Open przyjmuje nazwę pliku albo TStream, wykrywa format, raz ładuje współdzielone ciągi i metadane stylów dat i wybiera arkusz 1. SelectSheet (od jedynki) albo SelectSheetByName przecelowuje inny arkusz i resetuje kursor do przed-pierwszego. FindFirst i FindNext pozycjonują potem na następnym zaludnionym wierszu — wiersze bez dekodowalnych komórek są pomijane, więc RowIndex może skakać — a bieżący wiersz jest eksponowany jako CellCount, Cells[] i ValueByCol[], wszystkie od jedynki na osi kolumn. Wyjście z pętli to Break
var
Cursor: TXLSRowCursor;
Hits: Integer;
begin
Cursor := TXLSRowCursor.Create;
try
Cursor.FirstRow := 2; // pomiń pas nagłówka
Cursor.IncludeColumn(1); // dekoduj tylko te dwie kolumny
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; // zwykły Break; żadnej flagi abort, żadnego wartownika
end;
until not Cursor.FindNext;
finally
Cursor.Free; // destruktor kończy przebieg
end;
end;
Projekcja i zakres są ustawiane przed przebiegiem, nie filtrowane potem. FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates i DetectTextTypes są wszystkie honorowane wewnątrz backendów, więc niewybrana kolumna nigdy nie alokuje swojej wartości, ciągu formuły ani ładunku rich-text w ogóle — zestaw regresji dowodzi tego formułami 16 KiB i buforowanymi ciągami, które nigdy nie są materializowane, gdy ich kolumna nie jest rzutowana. Te opcje są celowo zamrożone, dopóki przebieg jest aktywny, i znów stają się zapisywalne przy EOF, przy SelectSheet albo po Close, więc jeden skan nigdy nie zmiesza dwóch kontraktów dekodowania. Jeśli potrzebujesz tylko inwentarza arkuszy, a nie wierszy, ładowanie tylko-metadanych i selektywne arkuszy jest tańszym punktem wejścia
Jeden backend na format, jedna pętla skanowania każdy
Każdy format ma dokładnie jeden skaner forward wewnątrz HotXLS, i zarówno kursor pull, jak i czytnik callbackowy napędzają ten sam skaner. TXLSXForwardRowBackend jest jedyną maszyną stanów SAX arkusza dla części arkuszy ECMA-376 Part 1 §18.3, trzyma czytnik XML, tabelę formuł współdzielonych i parser rich-text i przesuwa się dokładnie do jednej fizycznej granicy <row> na wywołanie. TXLSBiffForwardParser jest właścicielem globals, wyboru arkusza i przesuwu wiersza dla strumienia rekordów [MS-XLS]; uczynienie go pauzowalnym wyprodukowało najostrzejsze ograniczenie całego projektu, bo buforowana formuła ciągowa to rekord Formula, po którym natychmiast następuje rekord String, więc punkt zawieszenia per wiersz nigdy nie może wylądować między nimi. TXLSForwardTextBackend trzyma czytnika świadomego BOM, aktywny delimiter i jeden rekord logiczny — CSV węszy przecinkiem, średnikiem, tabulatorem albo pipe z pierwszego rekordu, ignorując znaki w cudzysłowach, a wielowierszowe pola w cudzysłowach są łączone przez #10, więc numer wiersza śledzi rekordy logiczne, a nie fizyczne nowe linie. TXLSForwardOdsBackend trzyma jeden fizyczny szablon wiersza dla tabel OpenDocument §9, traktuje table:number-rows-repeated jako licznik pozostałych, a nie rozwinięcie, i przesuwa się ponad pokrytymi komórkami bez emitowania wartości. Strumieniowy czytnik bezpośredni dzieli ten sam loader współdzielonych ciągów i stylów dat
Dlaczego sześć stanów zamiast jednej flagi Eof?
Bo pojedynczy boolean czyni cztery różne sytuacje nierozróżnialnymi, a wywołujący zgadują źle co do wszystkich. TXLSRowCursorState nazywa je jawnie
xrcsClosed— żadne źródło nie jest otwartexrcsBeforeFirst— otwarty albo przecelowany, jeszcze żaden wiersz nie przeczytanyxrcsActive— stoi na poprawnym wierszuxrcsEof— arkusz został skonsumowany do końcaxrcsCancelled— wywołujący zatrzymał przebieg celowoxrcsFaulted— przebieg zawiódł i pierwotny wyjątek został rzucony
To ostatnie rozróżnienie jest tym, co ma znaczenie w produkcji. Brakująca część arkusza albo zawiedziony start przebiegu zachowuje swój EReadError i przesuwa kursor do xrcsFaulted; nigdy nie jest degradowany do zwykłego False, które wywołujący przeczytałby jako „ten arkusz był pusty”. Cancel jest celowo węższy niż Close: zamyka backend bieżącego arkusza i jego substream rozompresowywania i unieważnia bieżący wiersz, ale nie uwalnia archiwum ZIP ani strumienia źródłowego, a wywołanie go dwa razy to no-op. Po anulowaniu wznawiasz przez jawne wywołanie SelectSheet — kursor nie zrestartuje po cichu przebiegu za ciebie. Własność strumienia trzyma tę samą defensywną regułę: xsoBorrowed jest domyślne i przywraca pozycję strumienia przy zamknięciu, xsoOwned przekazuje własność dopiero po tym, jak Open już się powiodło, więc nieudane otwarcie nigdy nie uwalnia strumienia, którego wywołujący wciąż się trzyma
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: kursor nigdy nie uwalnia Src, a Close przywraca
// pozycję, którą strumień miał, gdy wywołano Open
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // zamyka backend arkusza i jego
Break; // substream rozompresowywania; idempotentne
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; // wciąż nasz, wciąż ważny, pozycja przywrócona
end;
end;
Wypożyczanie bieżącego wiersza bez kopiowania
IXLSRowCursorView oddaje wiersz innej procedurze bez duplikowania tablicy komórek. Widok trzyma współdzieloną straż trzymającą wskaźnik kursora plus licznik pokoleń UInt64; przesuwanie, wybór arkusza, anulowanie, zamykanie i niszczenie kursora wszystkie inkrementują to pokolenie, a niszczenie dodatkowo czyści właściciela straży. Więc nieświeży widok nie może czytać uwolnionej pamięci: Valid to sonda bez wyjątków, którą możesz wywołać w każdej chwili, podczas gdy każdy inny członek najpierw waliduje i rzuca EXLSRowCursorViewInvalidated. Bądź szczery co do tego, czym ten kontrakt jest — to fail-fast czasu życia, nie gwarancja bezpieczeństwa wątków, i nie licencjonuje czytania wiersza z drugiego wątku, podczas gdy pierwszy przesuwa kursor
var
View: IXLSRowCursorView;
Cell: TXLSRowCursorCell;
I: Integer;
begin
if Cursor.FindFirst then
repeat
View := Cursor.CurrentRowView; // wypożycza; żadna tablica komórek nie jest kopiowana
for I := 0 to View.CellCount - 1 do
begin
Cell := View.Cells[I];
if Cell.HasFormula and not Cell.FormulaTextAvailable then
UseCachedResult(Cell.Value) // odczyty forward BIFF trzymają
else if Cell.Kind = xdkEmpty then // buforowany wynik, nie tokeny
UseStyleOnly(Cell.StyleIndex) // Blank / MulBlank to prawdziwe komórki
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// Interfejs przeżywa pętlę, ale wiersz za nim nie
if not View.Valid then // Valid nigdy nie rzuca; Cells[] teraz by rzuciło
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes i co wolno mu dowodzić
PeakRowBufferedBytes istnieje po to, by dowodzić, że pamięć śledzi szerokość wiersza, a nie liczbę wierszy. Kumuluje rekordy komórek, Varianty, ciągi formuł i ładunki rich-text bieżącego wiersza wyjściowego i włącza formatowo-specyficzny zestaw roboczy — logiczny rekord CSV, fizyczny szablon wiersza ODS, pik rekordów BIFF albo surową komórkę XLSX właśnie dekodowaną. Czytaj go razem z SheetPassesStarted, które liczy, ile przebiegów arkuszy faktycznie się zaczęło. Dwa zastrzeżenia utrzymują to uczciwie: liczba jest oszacowaniem, nie dokładną księgowością sterty, i jest monotoniczna od ostatniego Open, więc to instrument debugowania i regresji, a nie żywy wskaźnik. Dla szerszego obrazu, gdzie idzie czas i bajty na bardzo dużych skoroszytach, zobacz wydajność dużych skoroszytów w Delphi
Czytnik push stał się adapterem i czego kursor nie zrobi
TXLSForwardReader nie niesie już osobnych punktów wejścia skanowania XLSX, BIFF i tekstowego. Konfiguruje kursor, przechodzi go i tłumaczy bieżący wiersz na zdarzenia OnSheet i OnCell, dlatego dwie fasady nie mogą się już rozjeżdżać na filtrowaniu, stanie formuł ani obsłudze błędów. Dwa skutki są warte znajomości przed uaktualnieniem: callbackowy SheetIndex jest teraz jednolicie od jedynki na TXLSForwardReader (TXLSDirectReader zachowuje swój istniejący kontrakt zdarzeń od zera), a OnSheet strzela przed SelectSheet, więc ustawienie SkipSheet znaczy, że część arkusza nigdy nie jest otwierana ani rozompresowywana w ogóle. Granice są równie jawne: skoroszyt nie może być modyfikowany, dopóki przebieg jest aktywny, anulowanie wymaga jawnego restartu, a ścieżka forward BIFF nigdy nie dekompiluje tokenów formuł, więc klasyczne komórki formuł raportują HasFormula prawdziwe z FormulaTextAvailable fałszywym i podają buforowany wynik, zamiast wymyślać pusty ciąg formuły. Kursor wierszy i jego adapter przeszły 1 298 kontroli na Delphi Win32 i Win64 plus statycznym pakiecie C++Builder 37.0 Win64
Jeśli wahasz się między kursorem pull a loaderem, który masz teraz, pytanie nie brzmi, który parsuje szybciej, tylko który pozwala ci napisać warunek wyjścia, którego naprawdę potrzebujesz. Pełne szczegóły komponentu, wspierane wersje IDE i licencjonowanie są na stronie komponentu arkuszowego HotXLS Delphi