Τεχνικό Άρθρο

Δρομέας γραμμών pull για XLS, XLSX, ODS και CSV σε Delphi

Το 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 και στυλ ημερομηνιών

Ο pull row cursor του HotXLS διανέμει σε έναν forward scanner ανά μορφή — ένα SAX backend για XLSX, έναν record parser για BIFF, ένα text backend που ανιχνεύει delimiter και ένα πρότυπο γραμμής ODS — με τον callback reader ρυθμισμένο από πάνω ως adapter
Κάθε μορφή έχει ακριβώς έναν forward scanner, και τόσο ο pull cursor όσο και ο callback reader οδηγούν τον ίδιο scanner, οπότε η σημασιολογία φιλτραρίσματος και σφαλμάτων δεν μπορεί να αποκλίνει

Γιατί έξι καταστάσεις αντί για μία σημαία 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 που κρατά ακόμα ο καλών

Οι έξι καταστάσεις του row cursor του HotXLS με τις μεταβάσεις μεταξύ τους — το Cancel μεταφέρει ένα ενεργό πέρασμα σε cancelled, μια αποτυχημένη εκκίνηση περάσματος το μεταφέρει σε faulted, και πώς και τα δύο μένουν διακριτά από το τέλος του φύλλου
Έξι ονομασμένες καταστάσεις κρατούν διακριτά ένα κενό φύλλο, ένα σκόπιμο σταμάτημα και ένα αποτυχημένο πέρασμα, κάτι που ένα μοναδικό Eof boolean δεν μπορεί να κάνει
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

Σύγκριση του HotXLS όπου το φόρτωμα όλου του φύλλου κρατά κάθε γραμμή resident, ενώ ο pull cursor κρατά μόνο την τρέχουσα γραμμή συν ένα working set μορφής — αυτό ακριβώς συσσωρεύει και αναφέρει το PeakRowBufferedBytes
Το PeakRowBufferedBytes συσσωρεύει την τρέχουσα γραμμή εξόδου συν το working set της μορφής, οπότε η μνήμη μετράει πόσο φαρδιά είναι μια γραμμή αντί για πόσες γραμμές έχει το φύλλο

Ο 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