Το HotXLS διαβάζει πηγές .xls, .xlsx, .xlsm, .ods, CSV και TSV μέσα από έναν μόνο pull row cursor, το TXLSRowCursor, του οποίου τα FindFirst και FindNext προχωρούν μία λογική γραμμή τη φορά ενώ στη μνήμη μένει μόνο εκείνη η γραμμή. Ένα state machine έξι τιμών διαχωρίζει το before-first από το EOF, το cancelled και το faulted, και ο παλιότερος callback reader είναι πια ένα adapter πάνω στον ίδιο cursor
Το σενάριο είναι οικείο σε όποιον έχει παραδώσει λειτουργία import. Φτάνει ένα .xlsx των 200 MB, συνδέετε έναν handler OnCell, και η πρώτη απαίτηση μετά το «διαβάστε το» είναι «σταματήστε μετά τις πρώτες εκατό ανεστραμμένες καταχωρήσεις». Τώρα το σχήμα του κώδικά σας σάς πολεμά: ο βρόχος ζει μέσα στη βιβλιοθήκη, ο handler σας πρέπει να σηκώσει μια σημαία, κάθε επόμενο callback εξακολουθεί να εκτελείται μέχρι να το προσέξει ο parser, και η συσσωρευμένη κατάσταση — πόσα ταιριάσματα έχουμε ως τώρα, ποια στήλη ταίριαξε, τι να κάνετε μετά — πρέπει να ζει σε πεδία μιας κλάσης που υπάρχει μόνο για να έχει πού να καθίσει το callback. Τίποτα από όλα αυτά δεν είναι πρόβλημα parsing. Είναι πρόβλημα ροής ελέγχου, και είναι αυτό που ένα pull cursor αφαιρεί
Πόσο κοστίζει πραγματικά ένα push callback στα 200 MB
Το push αντιστρέφει τον έλεγχο, και η αντιστροφή είναι ακριβώς ό,τι δεν αντέχει ένας καλών που φιλτράρει ή ενώνει. Με callback API ο βρόχος ανήκει στη βιβλιοθήκη, οπότε ο καλών δεν μπορεί να χρησιμοποιήσει Break, δεν μπορεί να εναλλάσσει δύο πηγές, δεν μπορεί να δώσει τον reader σε μια ρουτίνα που περιμένει να την οδηγούν, και δεν μπορεί να εκφράσει το «δείτε την επόμενη γραμμή πριν αποφασίσετε» χωρίς buffering. Το κόστος δεν είναι το throughput — ένα καλογραμμένο SAX callback path κάνει streaming μια χαρά — είναι ότι κάθε μη τετριμμένος καταναλωτής αναπτύσσει ένα δικό του μικρό state machine για να προσομοιώσει τον βρόχο που δεν του επέτρεψαν να γράψει. Πολλαπλασιάστε το επί τέσσερις μορφές αρχείων, καθεμία ιστορικά με το δικό της σημείο εισόδου σάρωσης, και η σημασιολογία φιλτραρίσματος, τύπων και σφαλμάτων αρχίζει να αποκλίνει μεταξύ τους — που είναι ακριβώς η απόκλιση που το HotXLS ξεκίνησε να κλείσει
Πώς αλλάζει ένα pull cursor τον κώδικα κλήσης;
Σας επιστρέφει τον βρόχο, και μαζί του τη συνηθισμένη ροή ελέγχου της Pascal. Το TXLSRowCursor.Open δέχεται όνομα αρχείου ή TStream, ανιχνεύει τη μορφή, φορτώνει μία φορά τα shared strings και τα μεταδεδομένα στυλ ημερομηνιών, και επιλέγει το φύλλο 1. Το SelectSheet (1-based) ή το SelectSheetByName στοχεύει σε άλλο φύλλο εργασίας και επαναφέρει τον cursor στο before-first. Τα FindFirst και FindNext τοποθετούνται τότε στην επόμενη συμπληρωμένη γραμμή — οι γραμμές χωρίς αποκωδικοποιήσιμα κελιά παραλείπονται, οπότε το RowIndex μπορεί να πηδήξει — και η τρέχουσα γραμμή εκτίθεται ως CellCount, Cells[] και ValueByCol[], όλα 1-based στον άξονα των στηλών. Η έξοδος από τον βρόχο είναι ένα 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, χωρίς σημαία abort, χωρίς sentinel
end;
until not Cursor.FindNext;
finally
Cursor.Free; // ο destructor τερματίζει το πέρασμα
end;
end;
Η προβολή και το εύρος ορίζονται πριν από το πέρασμα, δεν φιλτράρονται μετά. Τα FirstRow, LastRow, IncludeColumn, ClearColumnProjection, IncludeFormulaText, DetectDates και DetectTextTypes τηρούνται όλα μέσα στα backends, οπότε μια μη επιλεγμένη στήλη δεν δεσμεύει ποτέ εξαρχής την τιμή της, το string τύπου της ή το rich-text payload της — η σουίτα regression το αποδεικνύει με τύπους 16 KiB και cached strings που δεν υλοποιούνται ποτέ όταν η στήλη τους δεν προβάλλεται. Οι επιλογές αυτές παγώνουν σκόπιμα όσο ένα πέρασμα είναι ενεργό και γίνονται ξανά εγγράψιμες στο EOF, στο SelectSheet, ή μετά το Close, ώστε μία σάρωση να μην ανακατεύει ποτέ δύο συμβόλαια αποκωδικοποίησης. Αν χρειάζεστε μόνο την απογραφή των φύλλων αντί για τις γραμμές, το metadata-only και επιλεκτικό φόρτωμα φύλλων είναι το φθηνότερο σημείο εισόδου
Ένα backend ανά μορφή, ένας βρόχος σάρωσης για καθεμία
Κάθε μορφή έχει ακριβώς έναν forward scanner μέσα στο HotXLS, και τόσο ο pull cursor όσο και ο callback reader οδηγούν τον ίδιο scanner. Το TXLSXForwardRowBackend είναι το μοναδικό SAX state machine φύλλου εργασίας για τα sheet parts του ECMA-376 Part 1 §18.3, κρατά τον XML reader, τον πίνακα shared formulas και τον rich-text parser, και προχωρά σε ακριβώς ένα φυσικό όριο <row> ανά κλήση. Το TXLSBiffForwardParser κατέχει τα globals, την επιλογή φύλλου και την προώθηση γραμμής για τη ροή εγγραφών [MS-XLS] — το να γίνει pausable παρήγαγε τον πιο αυστηρό περιορισμό όλου του σχεδιασμού, επειδή μια cached string formula είναι μια εγγραφή Formula που ακολουθείται αμέσως από μια εγγραφή String, οπότε ένα σημείο αναστολής ανά γραμμή δεν πρέπει ποτέ να πέσει μεταξύ των δύο. Το TXLSForwardTextBackend κρατά έναν BOM-aware reader, το ενεργό delimiter και μία λογική εγγραφή — το CSV ανιχνεύει κόμμα, semicolon, tab ή pipe από την πρώτη εγγραφή αγνοώντας τους χαρακτήρες σε εισαγωγικά, και τα πολυγραμμικά πεδία σε εισαγωγικά ενώνονται με #10 ώστε ο αριθμός γραμμής να μετρά λογικές εγγραφές αντί για φυσικά newlines. Το TXLSForwardOdsBackend κρατά ένα μόνο φυσικό πρότυπο γραμμής για τους πίνακες OpenDocument §9, αντιμετωπίζει το table:number-rows-repeated ως μετρητή υπολοίπου παρά ως επέκταση, και προχωρά πέρα από τα covered cells χωρίς να εκπέμπει τιμές. Ο streaming direct reader μοιράζεται τον ίδιο φορτωτή shared strings και στυλ ημερομηνιών
Γιατί έξι καταστάσεις αντί για μία σημαία Eof;
Επειδή ένα απλό boolean κάνει τέσσερις διαφορετικές καταστάσεις αδιαχώριστες, και οι καλούντες μαντεύουν λάθος για όλες. Το TXLSRowCursorState τις ονομάζει ρητά
xrcsClosed— δεν είναι ανοιχτή καμία πηγήxrcsBeforeFirst— άνοιξε ή ξαναστοχεύτηκε, δεν έχει διαβαστεί ακόμα γραμμήxrcsActive— στέκεται σε έγκυρη γραμμήxrcsEof— το φύλλο καταναλώθηκε ως το τέλοςxrcsCancelled— ο καλών σταμάτησε το πέρασμα σκόπιμαxrcsFaulted— το πέρασμα απέτυχε και η αρχική εξαίρεση εκτοξεύτηκε
Αυτή η τελευταία διάκριση είναι αυτή που μετράει στην παραγωγή. Ένα worksheet part που λείπει ή μια αποτυχημένη εκκίνηση περάσματος κρατά το EReadError του και μεταφέρει τον cursor στο xrcsFaulted — ποτέ δεν υποβαθμίζεται σε ένα σκέτο False που ένας καλών θα το διάβαζε ως «αυτό το φύλλο ήταν κενό». Το Cancel είναι σκόπιμα στενότερο από το Close: κλείνει το τρέχον backend φύλλου εργασίας και το inflate substream του και ακυρώνει την τρέχουσα γραμμή, αλλά δεν απελευθερώνει το ZIP archive ή το source stream, και το να κληθεί δύο φορές είναι no-op. Μετά από ένα cancel συνεχίζετε καλώντας ρητά το SelectSheet — ο cursor δεν θα ξεκινήσει σιωπηλά πέρασμα εκ μέρους σας. Η ιδιοκτησία του stream ακολουθεί τον ίδιο αμυντικό κανόνα: το xsoBorrowed είναι η προεπιλογή και επαναφέρει τη θέση του stream στο κλείσιμο, το xsoOwned μεταφέρει την ιδιοκτησία μόνο αφού το Open έχει ήδη πετύχει, οπότε ένα αποτυχημένο open δεν απελευθερώνει ποτέ ένα stream που κρατά ακόμα ο καλών
var
Cursor: TXLSRowCursor;
Src: TFileStream;
begin
Src := TFileStream.Create('quarter.ods', fmOpenRead or fmShareDenyWrite);
try
Cursor := TXLSRowCursor.Create;
try
// xsoBorrowed: ο cursor δεν απελευθερώνει ποτέ το Src, και το Close
// επαναφέρει τη θέση που είχε το stream όταν κλήθηκε το Open
if not Cursor.Open(Src, xffAuto, xsoBorrowed) then
Exit;
if Cursor.FindFirst then
repeat
if UserPressedStop then
begin
Cursor.Cancel; // κλείνει το backend του φύλλου εργασίας και το
Break; // inflate substream του μόνο, idempotent
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 παραδίδει μια γραμμή σε άλλη ρουτίνα χωρίς να διπλασιάσει τον πίνακα κελιών. Η προβολή αποθηκεύει έναν κοινόχρηστο guard που κρατά τον δείκτη του cursor συν έναν μετρητή γενιάς UInt64. Η προώθηση, η επιλογή φύλλου, το cancel, το κλείσιμο και η καταστροφή του cursor αυξάνουν αυτή τη γενιά, και η καταστροφή επιπλέον μηδενίζει τον κάτοχο του guard. Έτσι μια ξεπερασμένη προβολή δεν μπορεί να διαβάσει ελευθερωμένη μνήμη: το Valid είναι μια δοκιμή χωρίς εξαιρέσεις που μπορείτε να καλέσετε ανά πάσα στιγμή, ενώ κάθε άλλο μέλος επικυρώνει πρώτα και εκτοξεύει EXLSRowCursorViewInvalidated. Να είστε τίμιοι για το τι είναι αυτό το συμβόλαιο — είναι fail-fast έλεγχος διάρκειας ζωής, όχι εγγύηση thread-safety, και δεν δίνει άδεια να διαβάζετε γραμμή από δεύτερο thread όσο το πρώτο προωθεί τον cursor
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 // το cached αποτέλεσμα, όχι τα tokens
UseStyleOnly(Cell.StyleIndex) // τα Blank / MulBlank είναι πραγματικά κελιά
else
UseValue(Cell.Col, Cell.Value);
end;
until not Cursor.FindNext;
// Το interface επιζεί του βρόχου, αλλά η γραμμή από πίσω του όχι
if not View.Valid then // το Valid δεν εκτοξεύει ποτέ, το Cells[] τώρα θα εκτοξεύσει
View := nil; // EXLSRowCursorViewInvalidated
end;
PeakRowBufferedBytes, και τι επιτρέπεται να αποδείξει
Το PeakRowBufferedBytes υπάρχει για να δείξει ότι η μνήμη ακολουθεί το πλάτος της γραμμής αντί για τον αριθμό των γραμμών. Συσσωρεύει τις εγγραφές κελιών, τα Variants, τα strings τύπων και τα rich-text payloads της τρέχουσας γραμμής εξόδου και ενσωματώνει το working set της μορφής — τη λογική εγγραφή CSV, το φυσικό πρότυπο γραμμής ODS, την κορύφωση εγγραφών BIFF, ή το ακατέργαστο κελί XLSX που αποκωδικοποιείται αυτή τη στιγμή. Διαβάστε το μαζί με το SheetPassesStarted, που μετρά πόσα περάσματα φύλλων ξεκίνησαν πραγματικά. Δύο επιφυλάξεις το κρατούν τίμιο: το νούμερο είναι εκτίμηση, όχι ακριβής λογιστική του heap, και είναι μονότονο από το πιο πρόσφατο Open, οπότε είναι όργανο debugging και regression παρά ζωντανή μέτρηση. Για την ευρύτερη εικόνα του πού πηγαίνουν χρόνος και bytes σε πολύ μεγάλα βιβλία εργασίας, δείτε την απόδοση μεγάλων βιβλίων εργασίας σε Delphi
Ο push reader έγινε adapter, και τι δεν θα κάνει ο cursor
Το TXLSForwardReader δεν κουβαλά πια ξεχωριστά σημεία εισόδου σάρωσης για XLSX, BIFF και κείμενο. Ρυθμίζει έναν cursor, τον διασχίζει, και μεταφράζει την τρέχουσα γραμμή σε γεγονότα OnSheet και OnCell, γι' αυτό τα δύο facades δεν μπορούν πια να αποκλίνουν σε φιλτράρισμα, κατάσταση τύπων ή διαχείριση σφαλμάτων. Δύο συνέπειες αξίζει να ξέρετε πριν αναβαθμίσετε: το callback SheetIndex είναι πια ομοιόμορφα 1-based στο TXLSForwardReader (το TXLSDirectReader κρατά το υπάρχον συμβόλαιο events 0-based), και το OnSheet εκτελείται πριν από το SelectSheet, οπότε όταν ορίσετε SkipSheet το worksheet part δεν ανοίγεται και δεν αποσυμπιέζεται καθόλου. Τα όρια είναι εξίσου ρητά: το βιβλίο εργασίας δεν πρέπει να τροποποιείται όσο ένα πέρασμα είναι ενεργό, το cancel απαιτεί ρητή επανεκκίνηση, και το BIFF forward path δεν απομεταφράζει ποτέ formula tokens, οπότε τα κλασικά κελιά τύπων αναφέρουν HasFormula true με FormulaTextAvailable false και σας δίνουν το cached αποτέλεσμα αντί να επινοήσουν κενό string τύπου. Ο row cursor και το adapter του πέρασαν 1.298 ελέγχους σε Delphi Win32 και Win64 συν το static package C++Builder 37.0 Win64
Αν ζυγίζετε έναν pull cursor απέναντι στον loader που έχετε τώρα, η ερώτηση που πρέπει να κάνετε δεν είναι ποιος αναλύει πιο γρήγορα αλλά ποιος σάς επιτρέπει να γράψετε τη συνθήκη εξόδου που πραγματικά χρειάζεστε. Πλήρεις λεπτομέρειες για το component, τις υποστηριζόμενες εκδόσεις IDE και την αδειοδότηση βρίσκονται στη σελίδα του HotXLS Delphi spreadsheet component