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

Εξαγωγή Πινάκων από Σελίδες PDF στη Delphi με το PDFium

Το PDFium Component εντοπίζει πίνακες σε μια σελίδα PDF και τους επιστρέφει ως πλέγμα κελιών με εκτάσεις γραμμών και στηλών, γραμμές κεφαλίδας και μια τιμή εμπιστοσύνης, μέσω της ExtractTables για μία σελίδα και της ExtractDocumentTables για ολόκληρο έγγραφο. Κάθε πίνακας μετατρέπεται σε CSV ή JSON με μία κλήση, και πίνακες που συνεχίζονται πέρα από μια αλλαγή σελίδας μπορούν να συνδεθούν σε μια αλυσίδα συνέχειας

Το PDF δεν έχει αντικείμενο πίνακα. Ένας πίνακας σε ένα PDF είναι ένα σύνολο text runs τοποθετημένων ώστε ένας άνθρωπος να τα διαβάζει ως πλέγμα, μερικές φορές με γραμμές σχεδιασμένες γύρω τους και συχνά χωρίς. Η ανάκτηση του πλέγματος σημαίνει ανακατασκευή μιας πρόθεσης που το αρχείο δεν κατέγραψε ποτέ, γι' αυτό κάθε εργαλείο εξαγωγής παράγει ελαφρώς διαφορετικά αποτελέσματα και γι' αυτό ένα εργαλείο που σας λέει την εμπιστοσύνη του είναι πιο χρήσιμο από ένα που δεν το κάνει

Δύο λειτουργίες εντοπισμού για δύο είδη πίνακα

Ο εντοπισμός με γραμμές χρησιμοποιεί τις σχεδιασμένες γραμμές. Κάθε τμήμα διαδρομής με περίγραμμα μετασχηματίζεται σε συντεταγμένες σελίδας μέσω του πίνακα του αντικειμένου σελίδας, οι οριζόντιες και κάθετες γραμμές τέμνονται, και οι τομές σχηματίζουν συνδεδεμένα στοιχεία. Κάθε στοιχείο γίνεται το δικό του ταξινομημένο πλέγμα θέσεων X και Y, κάτι που είναι αυτό που εμποδίζει δύο ξεχωριστούς πίνακες στην ίδια σελίδα να συγχωνευτούν σε ένα παράλογο πλέγμα

Ο εντοπισμός με κενά διαστήματα (whitespace) χειρίζεται πίνακες σχεδιασμένους με ευθυγράμμιση αντί για γραμμές. Τα πλαίσια λέξεων ομαδοποιούνται σε οπτικές γραμμές, τα κενά μέσα σε μια γραμμή τη χωρίζουν σε υποψήφιες στήλες, και ένας πίνακας γίνεται αποδεκτός μόνο όταν τουλάχιστον MinRows γραμμές επαναλαμβάνουν τουλάχιστον MinColumns αγκυρώσεις ευθυγραμμισμένες αριστερά μέσα σε AlignmentTolerance. Ο παράγοντας κενού γραμμής προεπιλέγεται σε 3, κάτι που καλύπτει την περίπου 30 σημείων απόσταση γραμμής βάσης τυπική για κείμενο 12 σημείων χωρίς να επιτρέπει σε μία γραμμή με αρκετά text runs να μεταμφιεστεί σε πίνακα

Διάγραμμα της διοχέτευσης εντοπισμού πινάκων του PDFium Component στη Delphi, όπου οι τομές σχεδιασμένων γραμμών και οι ευθυγραμμισμένες με κενά γραμμές λέξεων τροφοδοτούν μία βαθμολογημένη εγγραφή πίνακα με εξαγωγή CSV και JSON
Ο εντοπισμός με γραμμές τέμνει σχεδιασμένες γραμμές ενώ ο εντοπισμός με κενά μετρά ευθυγραμμισμένες γραμμές πλαισίων λέξεων· οι υποψήφιοι που ξεπερνούν τα MinRows και MinColumns αναδύονται με βαθμολογία εμπιστοσύνης και το DetectionMode προσαρτημένο
uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'annual-report.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 12;                    // 1-based

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // default is 0.5
    Options.HeaderRowCount := 1;

    Tables := Pdf.ExtractTables(Options);
    for I := 0 to High(Tables) do
      Writeln(Format('table %d: %dx%d cells, confidence %.2f, mode %d',
        [I, Tables[I].RowCount, Tables[I].ColumnCount,
         Tables[I].Confidence, Ord(Tables[I].DetectionMode)]));

    if Length(Tables) > 0 then
      SaveText('page12-table0.csv', Tables[0].ToCsv);
  finally
    Pdf.Free;
  end;
end;

Πώς ανακτώνται τα συγχωνευμένα κελιά;

Αυτό είναι το σημείο όπου οι αφελείς εξαγωγείς κάνουν λάθος. Ένα συγχωνευμένο κελί δεν μπορεί να αναγνωριστεί μόνο από το καθολικό πλέγμα, επειδή το πλέγμα προκύπτει από όλες τις γραμμές στη σελίδα και μια συγχωνευμένη περιοχή απλά στερείται την εσωτερική γραμμή που θα την είχε χωρίσει

Ο κανόνας που χρησιμοποιείται εδώ είναι τοπικός: δύο γειτονικά βασικά κελιά συγχωνεύονται όταν καμία οριακή γραμμή δεν καλύπτει το διάστημα ανάμεσά τους. Η δομή union-find τα ενώνει, τα προκύπτοντα ορθογώνια στοιχεία γίνονται τιμές RowSpan και ColumnSpan, και το κείμενο αντιστοιχίζεται σε ένα βασικό κελί από το κεντρικό του σημείο και έπειτα ακολουθεί αυτό το κελί έως τη ρίζα συγχώνευσής του. Αυτός ο τρόπος διατηρεί επίσης το κόστος γραμμικό ως προς τις λέξεις συν τα κελιά, αντί για την τετραγωνική σάρωση που προκύπτει από τον έλεγχο κάθε λέξης έναντι κάθε κελιού

Διάγραμμα ανάκτησης συγχωνευμένων κελιών στην εξαγωγή πινάκων PDFium για τη Delphi, όπου η δομή union-find ενώνει γειτονικά βασικά κελιά όποτε καμία οριακή γραμμή δεν καλύπτει το διάστημα ανάμεσά τους, παράγοντας RowSpan και ColumnSpan
Η δομή union-find συγχωνεύει γειτονικά βασικά κελιά των οποίων το κοινό διάστημα δεν φέρει σχεδιασμένο όριο, ώστε μια συγχωνευμένη κεφαλίδα να επιστρέφει ως ένα κελί με ορισμένο ColumnSpan αντί για ένα συμπληρωμένο κελί πλαισιωμένο από κενά

Το πρακτικό αποτέλεσμα είναι ότι ένας οικονομικός πίνακας με μια συγχωνευμένη κεφαλίδα "Total" που εκτείνεται σε τρεις στήλες βγαίνει με ένα κελί έκτασης τρία, αντί για ένα συμπληρωμένο κελί και δύο μυστηριώδη κενά

Συνέχεια πέρα από σελίδες

Οι μεγάλοι πίνακες σπάνε πέρα από σελίδες, και η αντιμετώπιση του τμήματος κάθε σελίδας ως ανεξάρτητου πίνακα αναγκάζει τον καλούντα να τα ράψει μεταξύ τους. Η ExtractDocumentTables μπορεί να τα συνδέσει αντ' αυτού, αλλά μόνο υπό αυστηρές συνθήκες: το τμήμα πρέπει να είναι ο χαμηλότερος πίνακας στην προηγούμενη σελίδα, ο επόμενος πρέπει να είναι ο ανώτατος πίνακας στην επόμενη σελίδα, οι αριθμοί σελίδων πρέπει να είναι γειτονικοί, και τα όρια στηλών πρέπει να ταιριάζουν

Και οι τέσσερις συνθήκες μαζί είναι αυτό που αποτρέπει το προφανές σφάλμα, δηλαδή την αλυσιδωτή σύνδεση κάθε τετράστηλου πίνακα σε ένα έγγραφο σε έναν φανταστικό μεγα-πίνακα επειδή τυχαίνει να μοιράζονται αριθμό στηλών. Όταν ισχύουν οι συνθήκες, οι πίνακες μοιράζονται ένα αναγνωριστικό ομάδας συνέχειας και φέρουν μεταδεδομένα συνέχειας· όταν δεν ισχύουν, λαμβάνετε ξεχωριστούς πίνακες και μπορείτε να αποφασίσετε μόνοι σας

Διάγραμμα συνέχειας πίνακα πέρα από σελίδες PDF στη Delphi, όπου τέσσερις αυστηρές πύλες αποφασίζουν αν το χαμηλότερο τμήμα σε μια σελίδα ενώνεται με το ανώτατο τμήμα στην επόμενη
Η εξαγωγή σε επίπεδο εγγράφου συνδέει τμήματα μόνο όταν ισχύουν και οι τέσσερις πύλες, εμποδίζοντας άσχετους τετράστηλους πίνακες από το να συγχωνευτούν σε έναν φανταστικό μεγα-πίνακα

Η εξαγωγή σε επίπεδο εγγράφου μοιράζεται τα όρια MaxCells και MaxTables μεταξύ σελίδων αντί να τα επαναφέρει ανά σελίδα, και αποκαθιστά την ενεργή σελίδα σε ένα μπλοκ finally, ώστε μια εκτέλεση εξαγωγής σε έναν viewer να αφήνει τον χρήστη να κοιτάζει τη σελίδα στην οποία βρισκόταν

Εξαγωγή χωρίς αλλοίωση των δεδομένων

Και οι δύο εξαγωγείς είναι σκόπιμοι ως προς τη διαφυγή χαρακτήρων (escaping). Το CSV πάντα βάζει τα πεδία σε εισαγωγικά και διπλασιάζει τα εσωτερικά εισαγωγικά, κάτι που αποφεύγει την κλασική αποτυχία όπου ένα κελί που περιέχει κόμμα γίνεται σιωπηλά δύο στήλες. Για συγχωνευμένα κελιά, το περιεχόμενο εκπέμπεται μόνο στην πάνω-αριστερή αγκύρωση, ώστε ένας πλήρης κύκλος CSV να μην διπλασιάζει μια κεφαλίδα που εκτείνεται στις στήλες που καλύπτει

Το JSON διατηρεί το Unicode αντί να το μετατρέπει σε ASCII, κάνει escape στους χαρακτήρες ελέγχου, και περιλαμβάνει τα μεταδεδομένα που χρειάζεται ένας καταναλωτής για να κρίνει την ποιότητα: λειτουργία εντοπισμού, εμπιστοσύνη, όρια, τιμές έκτασης, σημαίες κεφαλίδας και πληροφορίες συνέχειας. Αν τροφοδοτείτε εξαγόμενους πίνακες σε ένα μεταγενέστερο σύστημα, προτιμήστε JSON, επειδή μια γραμμή CSV δεν μπορεί να σας πει ότι ο πίνακας από τον οποίο προήλθε βαθμολογήθηκε με εμπιστοσύνη 0,51:

// Document-wide extraction, keeping only tables worth trusting
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
begin
  if Tables[I].Confidence < 0.75 then
  begin
    Log(Format('page %d table needs review (%.2f)',
      [Tables[I].PageNumber, Tables[I].Confidence]));
    Continue;
  end;
  if Tables[I].ContinuationGroup > 0 then
    AppendToGroup(Tables[I].ContinuationGroup, Tables[I].ToJson)
  else
    EmitStandalone(Tables[I].ToJson);
end;

Ρύθμιση, και το να ξέρετε πότε να σταματήσετε

Τρεις ρυθμίσεις έχουν μεγαλύτερη σημασία από τις υπόλοιπες. Το MinConfidence είναι η πύλη ποιότητας, και το 0,5 είναι σκόπιμα επιεικές· αυξήστε το για αυτοματοποιημένη εισαγωγή δεδομένων και μειώστε το για ένα UI ελέγχου όπου ένας άνθρωπος επιβεβαιώνει κάθε αποτέλεσμα. Το MinColumnGap αποφασίζει τι μετράει ως όριο στήλης σε λειτουργία whitespace, και πυκνά τοποθετημένοι πίνακες σε πυκνές αναφορές μπορεί να χρειάζονται μείωσή του από την προεπιλογή των 12 σημείων. Το MaxRowGapFactor αποφασίζει πότε η κατακόρυφη απόσταση τερματίζει έναν πίνακα, κάτι που έχει σημασία για πίνακες με περιστασιακές κενές γραμμές

Να είστε ειλικρινείς σχετικά με τα όρια. Οι πίνακες με γραμμές εξάγονται αξιόπιστα. Οι καθαρά ευθυγραμμισμένοι πίνακες whitespace εξάγονται καλά. Πίνακες με περιστραμμένο κείμενο, εμφωλευμένους πίνακες, ή κελιά των οποίων το περιεχόμενο τυλίγεται σε κάτι που μοιάζει με άλλη γραμμή θα χρειαστούν έλεγχο ανεξάρτητα από το πώς έχουν ρυθμιστεί οι παράμετροι. Γι' αυτά, το μοντέλο δομημένου κειμένου σάς δίνει την ακατέργαστη ύλη για να χτίσετε έναν αναγνώστη ειδικού τομέα, που περιγράφεται στο μπλοκ δομημένου κειμένου και σειρά ανάγνωσης

Ένας χρήσιμος συνδυασμός: όταν ένα σαρωμένο έγγραφο δεν έχει καθόλου κείμενο, ο εντοπισμός πινάκων δεν έχει τίποτα να δουλέψει μέχρι να υπάρξει ένα επίπεδο κειμένου. Προσθέστε ένα πρώτα, όπως περιγράφεται στο προσθήκη αναζητήσιμου επιπέδου κειμένου σε σαρωμένα PDF, και έπειτα εξάγετε. Τα πλαίσια λέξεων που επιστρέφει ένας πάροχος OCR είναι ακριβώς η είσοδος που χρειάζεται ο εντοπισμός whitespace

Η εξαγωγή πινάκων, το δομημένο κείμενο και η αναδιάταξη ροής διαβάζουν όλα από το ίδιο μοντέλο σελίδας στη Delphi, τη C++Builder και τη Lazarus· το πλήρες API περιγράφεται στη σελίδα του PDFium Component για Delphi