web-dev-qa-db-ja.com

PDFBoxを使用したPDFファイル(特にテーブルを含む))の解析

PDF表形式データを含むファイルを解析する必要があります。 PDFBox を使用して、後で結果(文字列)を解析するためにファイルテキストを抽出しています。問題はたとえば、このようなテーブルを含むファイルがあります(7列:最初の2つは常にデータを持ち、1つのComplexity列のみがデータを持ち、1つのFinancing列しかありませんデータあり):

+----------------------------------------------------------------+
| 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|
+----------------------------------------------------------------+

次に、PDFBoxを使用します。

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

これらの2行のデータは次のように抽出されます。

xyz 12.43 12.4312.43
abc 1.56 1.561.56

最後の2つの数字の間に空白はありませんが、これは最大の問題ではありません。問題は、最後の2つの数字の意味がわからないことです:中、高、該当なし? MAC /その他、FAE?数字と列の間には関係がありません。

PDFBoxライブラリを使用する必要はないため、別のライブラリを使用するソリューションで問題ありません。私が欲しいのは、ファイルを解析し、解析された各数値の意味を知ることができるようにすることです。

63
matheus.emm

使用可能な形式でデータを抽出するアルゴリズムを考案する必要があります。使用するPDFライブラリに関係なく、これを行う必要があります。文字とグラフィックスは、一連のステートフル描画操作によって描画されます。つまり、画面上のこの位置に移動し、文字「c」。

org.Apache.pdfbox.pdfviewer.PDFPageDrawerを拡張し、strokePathメソッドをオーバーライドすることをお勧めします。そこから、水平線セグメントと垂直線セグメントの描画操作をインターセプトし、その情報を使用してテーブルの列と行の位置を決定できます。次に、テキスト領域を設定し、どの領域にどの数字/文字/文字が描画されるかを決定するという簡単な問題。領域のレイアウトがわかっているため、抽出されたテキストがどの列に属しているかを知ることができます。

また、視覚的に区切られたテキスト間にスペースがない場合があるのは、スペース文字がPDFによって描画されないことが非常に多いためです。代わりに、テキストマトリックスが更新され、「移動」の描画コマンドが発行されて、次の文字と「スペース幅」を最後の文字とは別に描画します。

幸運を。

18
purecharger

PDFファイルからテーブルを抽出するために多くのツールを使用していましたが、うまくいきませんでした。

そのため、pdfファイルの表形式データを解析するために、独自のアルゴリズム(名前はtraprange)を実装しました。

次に、いくつかのサンプルpdfファイルと結果を示します。

  1. 入力ファイル: sample-1.pdf 、結果: sample-1.html
  2. 入力ファイル: sample-4.pdf 、結果: sample-4.html

traprange のプロジェクトページにアクセスします。

12
Tho

PDFBoxでエリアごとにテキストを抽出できます。 Mavenを使用している場合は、_ExtractByArea.Java_アーティファクトの_pdfbox-examples_サンプルファイルを参照してください。スニペットは次のようになります

_   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" );
_

問題は、そもそも座標を取得することです。通常のTextStripperを拡張し、processTextPosition(TextPosition text)をオーバーライドし、各文字の座標を出力し、ドキュメント内の位置を把握することに成功しました。

しかし、少なくともMacを使用している場合は、もっと簡単な方法があります。プレビューでPDFを開き、⌘Iでインスペクターを表示します。[切り抜き]タブを選択し、単位がポイントにあることを確認します。領域を選択すると、インスペクターに座標が表示され、それをRectangleコンストラクター引数に丸めて入力できます。最初の方法を使用して、Originの場所を確認するだけです。

11

私の答えには手遅れかもしれませんが、これはそれほど難しくないと思います。 PDFTextStripperクラスを拡張し、writePage()およびprocessTextPosition(...)メソッドをオーバーライドできます。あなたの場合、列ヘッダーは常に同じであると仮定します。つまり、各列見出しのx座標がわかっているので、数値のx座標を列見出しのx座標と比較できます。それらが十分近い場合(どれだけ近いかを判断するためにテストする必要があります)、その数はその列に属していると言うことができます。

別のアプローチは、各ページが書き込まれた後に「charactersByArticle」ベクトルをインターセプトすることです。

@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
}

列を知っていれば、x座標を比較して、すべての数値が属する列を決定できます。

数字の間にスペースがないのは、Wordの区切り文字列を設定する必要があるためです。

これがあなたや、似たようなことをしようとしている人に役立つことを願っています。

11
impeto

データの形式を保持するために設計された PDFLayoutTextStripper があります。

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

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

データが表形式であるpdfファイルの読み取りでも同じ問題がありました。 PDFBoxを使用した通常の解析の後、各行はセパレータとしてコンマを使用して抽出されました...列の位置を失いました。これを解決するために、PDFTextStripperByAreaを使用し、座標を使用して、各行の列ごとにデータを抽出しました。 これは、固定形式のpdfがある場合に提供されます。

        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" ));

次に、2行目など...

2
manu

PDFからデータを抽出すると、問題が発生します。ドキュメントは何らかの自動プロセスで作成されますか?その場合、PDFを非圧縮PostScriptに変換(pdf2psを試して) PostScriptに何らかの悪用可能な通常のパターンが含まれている場合。

2
Todd Owen

PDFBoxの PDFTextStripperByArea クラスを使用して、ドキュメントの特定の領域からテキストを抽出できます。これに基づいて、テーブルの各セルの領域を特定できます。これはそのままでは提供されませんが、例の DrawPrintTextLocations クラスは、ドキュメント内の個々の文字の境界ボックスを解析する方法を示しています(境界ボックスを解析するのは素晴らしいことです)文字列または段落がありますが、これについてはPDFBoxでサポートされていません。これを参照してください question )。このアプローチを使用して、すべての境界ボックスをグループ化して、テーブルの個別のセルを識別することができます。これを行う1つの方法は、boxesの_Rectangle2D_領域のセットを維持し、解析された各文字に対してDrawPrintTextLocations.writeString(String string, List<TextPosition> textPositions)のように文字の境界ボックスを見つけ、既存のコンテンツとマージすることです。 。

_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);
_

その後、これらの領域をPDFTextStripperByAreaに渡すことができます。

また、さらに進んでこれらの領域の水平成分と垂直成分を分離し、コンテンツを保持するかどうかに関係なく、すべてのテーブルのセルの領域を推測することもできます。

これらの手順を実行する理由があり、最終的に PDFBox を使用して独自のPDFTableStripperクラスを作成しました。 GitHubの要点 としてコードを共有しました。 main method は、クラスの使用方法の例を示します。

_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));
            }
        }
    }
}
_
2
beldaz

TabulaPDF( https://github.com/tabulapdf/tabula )を使用してみてください。これは、PDFファイルからテーブルコンテンツを抽出するのに非常に優れたライブラリです。予想どおりです。

幸運を。 :)

1
SURESH KUMAR S

http://swftools.org/ これらの人はpdf2swfコンポーネントを持っています。また、テーブルを表示することもできます。また、ソースも提供しています。そのため、おそらくそれをチェックアウトできます。

0
kaushalc

これは、PDFファイルにpdfbox 2.0.6を使用した「長方形テーブルのみ」がある場合に正常に機能します。長方形テーブルのみの他のテーブルでは機能しません。

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