web-dev-qa-db-fra.com

L'analyse PDF fichiers (en particulier avec des tableaux) avec PDFBox

Je dois analyser un fichier PDF contenant des données tabulaires. J'utilise PDFBox pour extraire le fichier texte afin d'analyser le résultat (String) ultérieurement. Le problème est que l'extraction de texte ne fonctionne pas comme prévu pour les données tabulaires. Par exemple, j'ai un fichier contenant un tableau comme celui-ci (7 colonnes: les deux premières ont toujours des données, une seule colonne de complexité contient des données, une seule colonne de financement contient des données):

+----------------------------------------------------------------+
| AIH | Value | Complexity                     | Financing       |
|     |       | Medium | High | Not applicable | MAC/Other | FAE |
+----------------------------------------------------------------+
| xyz | 12.43 | 12.34  |      |                | 12.34     |     |
+----------------------------------------------------------------+
| abc | 1.56  |        | 1.56 |                |           | 1.56|
+----------------------------------------------------------------+

Ensuite, j'utilise PDFBox:

PDDocument document = PDDocument.load(pathToFile);
PDFTextStripper s = new PDFTextStripper();
String content = s.getText(document);

Ces deux lignes de données seraient extraites comme ceci:

xyz 12.43 12.4312.43
abc 1.56 1.561.56

Il n'y a pas d'espace blanc entre les deux derniers chiffres, mais ce n'est pas le plus gros problème. Le problème est que je ne sais pas ce que signifient les deux derniers chiffres: moyen, élevé, non applicable? MAC/Autre, FAE? Je n'ai pas la relation entre les nombres et leurs colonnes.

Il n'est pas nécessaire pour moi d'utiliser la bibliothèque PDFBox. Une solution utilisant une autre bibliothèque convient donc. Ce que je veux, c'est pouvoir analyser le fichier et savoir ce que chaque numéro analysé signifie.

61
matheus.emm

Vous devrez concevoir un algorithme pour extraire les données dans un format utilisable. Quelle que soit la bibliothèque PDF que vous utilisez, vous devrez le faire. Les caractères et les graphiques sont dessinés par une série d’opérations de dessin avec état, c’est-à-dire qu’ils se déplacent jusqu’à cette position sur l’écran et dessinent le glyphe du caractère 'c'.

Je vous suggère d'étendre org.Apache.pdfbox.pdfviewer.PDFPageDrawer et de remplacer la méthode strokePath. À partir de là, vous pouvez intercepter les opérations de dessin pour les segments de ligne horizontaux et verticaux et utiliser ces informations pour déterminer les positions des colonnes et des lignes de votre tableau. Ensuite, il s’agit simplement de définir des régions de texte et de déterminer quels chiffres/lettres/caractères sont dessinés dans quelle région. Puisque vous connaissez la disposition des régions, vous serez en mesure de dire à quelle colonne appartient le texte extrait.

De plus, si vous ne pouvez pas laisser d'espaces entre les textes visuellement séparés, c'est que très souvent, les caractères d'espace ne sont pas dessinés par le fichier PDF. Au lieu de cela, la matrice de texte est mise à jour et une commande de dessin pour 'déplacer' est émise pour dessiner le caractère suivant et une "largeur d'espace" en dehors du dernier.

Bonne chance.

18
purecharger

J'avais utilisé de nombreux outils pour extraire un tableau à partir d'un fichier pdf, mais cela ne fonctionnait pas pour moi.

J'ai donc implémenté mon propre algorithme (son nom est traprange) pour analyser les données tabulaires dans des fichiers pdf. 

Voici quelques exemples de fichiers pdf et leurs résultats: 

  1. Fichier d'entrée: sample-1.pdf , résultat: sample-1.html
  2. Fichier d'entrée: sample-4.pdf , résultat: sample-4.html

Visitez ma page de projet à traprange .

12
Tho

Vous pouvez extraire le texte par zone dans PDFBox. Voir le fichier d'exemple ExtractByArea.Java dans l'artefact pdfbox-examples si vous utilisez Maven. Un extrait ressemble à

   PDFTextStripperByArea stripper = new PDFTextStripperByArea();
   stripper.setSortByPosition( true );
   Rectangle rect = new Rectangle( 464, 59, 55, 5);
   stripper.addRegion( "class1", rect );
   stripper.extractRegions( page );
   String string = stripper.getTextForRegion( "class1" );

Le problème est d'obtenir les coordonnées en premier lieu. J'ai réussi à étendre la TextStripper normale, à remplacer processTextPosition(TextPosition text) et à imprimer les coordonnées de chaque caractère et à déterminer où ils se trouvent dans le document.

Mais il existe un moyen beaucoup plus simple, du moins si vous êtes sur un Mac. Ouvrez le PDF dans Aperçu, ⌘I pour afficher l'inspecteur, choisissez l'onglet Rogner et assurez-vous que les unités sont en points. Dans le menu Outils, choisissez Sélection rectangulaire et sélectionnez la zone d'intérêt. Si vous sélectionnez une zone, l'inspecteur vous montrera les coordonnées que vous pourrez arrondir et ajouter aux arguments du constructeur Rectangle. Il vous suffit de confirmer l'emplacement de l'origine en utilisant la première méthode.

10
Emerson Farrugia

C'est peut-être trop tard pour ma réponse, mais je pense que ce n'est pas si difficile. Vous pouvez étendre la classe PDFTextStripper et remplacer les méthodes writePage () et processTextPosition (...). Dans votre cas, je suppose que les en-têtes de colonne sont toujours les mêmes. Cela signifie que vous connaissez la coordonnée x de chaque en-tête de colonne et que vous pouvez comparer la coordonnée x des nombres à celle des en-têtes de colonne. S'ils sont assez proches (vous devez tester pour décider de la proximité), vous pouvez dire que ce nombre appartient à cette colonne.

Une autre approche serait d’intercepter le vecteur "charactersByArticle" après l’écriture de chaque page:

@Override
public void writePage() throws IOException {
    super.writePage();
    final Vector<List<TextPosition>> pageText = getCharactersByArticle();
    //now you have all the characters on that page
    //to do what you want with them
}

Connaissant vos colonnes, vous pouvez faire votre comparaison des abscisses pour décider à quelle colonne appartient chaque numéro.

La raison pour laquelle vous n'avez pas d'espace entre les chiffres est parce que vous devez définir la chaîne de séparateur de mots.

J'espère que cela vous sera utile, à vous ou à d'autres personnes qui pourraient essayer des choses similaires.

10
impeto

Il y a PDFLayoutTextStripper qui a été conçu pour conserver le format des données.

Du README:

import Java.io.FileInputStream;
import Java.io.FileNotFoundException;
import Java.io.IOException;

import org.Apache.pdfbox.pdfparser.PDFParser;
import org.Apache.pdfbox.pdmodel.PDDocument;
import org.Apache.pdfbox.util.PDFTextStripper;

public class Test {

    public static void main(String[] args) {
        String string = null;
        try {
            PDFParser pdfParser = new PDFParser(new FileInputStream("sample.pdf"));
            pdfParser.parse();
            PDDocument pdDocument = new PDDocument(pdfParser.getDocument());
            PDFTextStripper pdfTextStripper = new PDFLayoutTextStripper();
            string = pdfTextStripper.getText(pdDocument);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        };
        System.out.println(string);
    }
}
7
Matthias Braun

J'ai eu un succès décent avec l'analyse des fichiers texte générés par l'utilitaire pdftotext (Sudo apt-get install poppler-utils).

File convertPdf() throws Exception {
    File pdf = new File("mypdf.pdf");
    String outfile = "mytxt.txt";
    String proc = "/usr/bin/pdftotext";
    ProcessBuilder pb = new ProcessBuilder(proc,"-layout",pdf.getAbsolutePath(),outfile); 
    Process p = pb.start();

    p.waitFor();

    return new File(outfile);
}
4
scott

J'ai eu le même problème en lisant le fichier pdf dans lequel les données sont au format tabulaire. Après une analyse régulière à l'aide de PDFBox, chaque ligne a été extraite avec une virgule comme séparateur ... perte de la position de colonne… .. Pour résoudre ce problème, j'ai utilisé PDFTextStripperByArea et, à l'aide de coordonnées, j'ai extrait les données colonne par colonne. Ceci est à condition que vous avez un format pdf fixe.

        File file = new File("fileName.pdf");
        PDDocument document = PDDocument.load(file);
        PDFTextStripperByArea stripper = new PDFTextStripperByArea();
        stripper.setSortByPosition( true );
        Rectangle rect1 = new Rectangle( 50, 140, 60, 20 );
        Rectangle rect2 = new Rectangle( 110, 140, 20, 20 );
        stripper.addRegion( "row1column1", rect1 );
        stripper.addRegion( "row1column2", rect2 );
        List allPages = document.getDocumentCatalog().getAllPages();
        PDPage firstPage = (PDPage)allPages.get( 2 );
        stripper.extractRegions( firstPage );
        System.out.println(stripper.getTextForRegion( "row1column1" ));
        System.out.println(stripper.getTextForRegion( "row1column2" ));

Puis la rangée 2 et ainsi de suite ...

2
manu

Extraire des données de PDF risque de poser de nombreux problèmes. Les documents sont-ils créés selon un processus automatique? Si tel est le cas, vous pouvez envisager de convertir les PDF au format PostScript non compressé (essayez pdf2ps) et de voir si le format PostScript contient une sorte de modèle normal que vous pouvez exploiter.

2
Todd Owen

Essayez d’utiliser TabulaPDF ( https://github.com/tabulapdf/tabula ). C'est une très bonne bibliothèque pour extraire le contenu de la table du fichier PDF. C'est très comme prévu.

Bonne chance. :)

1
SURESH KUMAR S

Vous pouvez utiliser PDFTextStripperByArea class de PDFBox pour extraire du texte d'une région spécifique d'un document. Vous pouvez vous appuyer sur ceci en identifiant la région de chaque cellule du tableau. Ceci n'est pas fourni directement, mais l'exemple DrawPrintTextLocations class montre comment analyser les cadres de sélection de caractères individuels dans un document (il serait bien d'analyser des cadres de sélection de chaînes ou de paragraphes, mais Je n'ai pas vu de support dans PDFBox pour cela - voir cette question ). Vous pouvez utiliser cette approche pour regrouper tous les cadres de sélection afin d'identifier des cellules distinctes d'un tableau. Une façon de le faire est de conserver un ensemble boxes de Rectangle2D régions, puis de rechercher pour chaque caractère analysé le cadre de sélection du caractère comme dans DrawPrintTextLocations.writeString(String string, List<TextPosition> textPositions) et de le fusionner avec le contenu existant.

Rectangle2D bounds = s.getBounds2D();
// Pad sides to detect almost touching boxes
Rectangle2D hitbox = bounds.getBounds2D();
final double dx = 1.0; // This value works for me, feel free to Tweak (or add setter)
final double dy = 0.000; // Rows of text tend to overlap, so no need to extend
hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy);
hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy);

// Find all overlapping boxes
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
for(Rectangle2D box: boxes) {
    if(box.intersects(hitbox)) {
        intersectList.add(box);
    }
}

// Combine all touching boxes and update
for(Rectangle2D box: intersectList) {
    bounds.add(box);
    boxes.remove(box);
}
boxes.add(bounds);

Vous pouvez ensuite passer ces régions à PDFTextStripperByArea.

Vous pouvez également aller plus loin et séparer les composants horizontaux et verticaux de ces régions, et ainsi en déduire des régions de toutes les cellules du tableau, que le contenu soit conservé ou non.

J'ai eu raison d'exécuter ces étapes et j'ai finalement écrit ma propre classe PDFTableStripper en utilisant PDFBox . J'ai partagé mon code en tant que Gist sur GitHub . La méthode main donne un exemple d'utilisation de la classe:

try (PDDocument document = PDDocument.load(new File(args[0])))
{
    final double res = 72; // PDF units are at 72 DPI
    PDFTableStripper stripper = new PDFTableStripper();
    stripper.setSortByPosition(true);

    // Choose a region in which to extract a table (here a 6"wide, 9" high rectangle offset 1" from top left of page)
    stripper.setRegion(new Rectangle(
        (int) Math.round(1.0*res), 
        (int) Math.round(1*res), 
        (int) Math.round(6*res), 
        (int) Math.round(9.0*res)));

    // Repeat for each page of PDF
    for (int page = 0; page < document.getNumberOfPages(); ++page)
    {
        System.out.println("Page " + page);
        PDPage pdPage = document.getPage(page);
        stripper.extractTable(pdPage);
        for(int c=0; c<stripper.getColumns(); ++c) {
            System.out.println("Column " + c);
            for(int r=0; r<stripper.getRows(); ++r) {
                System.out.println("Row " + r);
                System.out.println(stripper.getText(r, c));
            }
        }
    }
}
1
beldaz

http://swftools.org/ Ces gars ont un composant pdf2swf. Ils sont aussi capables de montrer des tableaux ..__ Ils donnent aussi la source. Donc, vous pouvez éventuellement le vérifier. 

0
kaushalc

Cela fonctionne très bien si le fichier PDF contient "Seulement une table rectangulaire" utilisant pdfbox 2.0.6. Ne fonctionnera pas avec une autre table uniquement Table rectangulaire.

import Java.io.File;
import Java.io.IOException;
import Java.util.ArrayList;

import org.Apache.pdfbox.pdmodel.PDDocument;
import org.Apache.pdfbox.text.PDFTextStripper;
import org.Apache.pdfbox.text.PDFTextStripperByArea;
public class PDFTableExtractor {
    public static void main(String[] args) throws IOException {
        ArrayList<String[]> objTableList = readParaFromPDF("C:\\sample1.pdf", 1,1,6);
        //Enter Filepath, startPage, EndPage, Number of columns in Rectangular table
    }
    public static ArrayList<String[]> readParaFromPDF(String pdfPath, int pageNoStart, int pageNoEnd, int noOfColumnsInTable) {
        ArrayList<String[]> objArrayList = new ArrayList<>();
        try {
            PDDocument document = PDDocument.load(new File(pdfPath));
            document.getClass();
            if (!document.isEncrypted()) {
                PDFTextStripperByArea stripper = new PDFTextStripperByArea();
                stripper.setSortByPosition(true);
                PDFTextStripper tStripper = new PDFTextStripper();
                tStripper.setStartPage(pageNoStart);
                tStripper.setEndPage(pageNoEnd);
                String pdfFileInText = tStripper.getText(document);
                // split by whitespace
                String Documentlines[] = pdfFileInText.split("\\r?\\n");
                for (String line : Documentlines) {
                    String lineArr[] = line.split("\\s+");
                    if (lineArr.length == noOfColumnsInTable) {
                        for (String linedata : lineArr) {
                            System.out.print(linedata + "             ");
                        }
                        System.out.println("");
                        objArrayList.add(lineArr);
                    }
                }
            }
        } catch (Exception e) {
            System.out.println("Exception " +e);
        }
            return objArrayList;
    }
}
0
Sunil K Chaudhary