使用PDFBoxparsingPDF文件(尤其是使用表格)

我需要parsing一个包含表格数据的PDF文件。 我正在使用PDFBox来提取文件文本以后parsing结果(string)。 问题是文本提取不像我预期的表格数据那样工作。 例如,我有一个包含这样一个表的文件(7列:前两个总是有数据,只有一个Complexity列有数据,只有一个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); 

这两行数据将被提取像这样:

 xyz 12.43 12.4312.43 abc 1.56 1.561.56 

最后两个数字之间没有空格,但这不是最大的问题。 问题是我不知道最后两个数字是什么意思:中,高,不适用? MAC /其他,FAE? 我没有数字和他们的列之间的关系。

我不需要使用PDFBox库,所以使用另一个库的解决scheme是好的。 我想要的是能够parsing文件,并知道每个parsing数字意味着什么。

您将需要devise一种algorithm来提取可用格式的数据。 无论您使用哪个PDF库,您都需要这样做。 字符和graphics是通过一系列有状态的绘图操作绘制的,即移动到屏幕上的这个位置并绘制字符“c”的字形。

我build议你扩展org.apache.pdfbox.pdfviewer.PDFPageDrawer并重写strokePath方法。 从那里,您可以截取水平和垂直线段的绘图操作,并使用该信息来确定表格的列和行位置。 然后它是一个简单的事情,build立文本区域,并确定在哪个区域绘制哪些数字/字母/字符。 由于您知道区域的布局,因此您可以知道提取的文本属于哪个列。

另外,在可视化分隔的文本之间可能没有空格的原因很多时候,空格字符不是由PDF绘制的。 而是更新文本matrix,发出“移动”的绘图命令以绘制下一个字符和与最后一个字符分开的“空间宽度”。

祝你好运。

我的回答可能为时已晚,但我认为这并不难。 您可以扩展PDFTextStripper类并覆盖writePage()和processTextPosition(…)方法。 在你的情况,我假设列标题始终是相同的。 这意味着您知道每个列标题的x坐标,您可以将数字的x坐标与列标题的x坐标进行比较。 如果他们足够接近(你必须testing以确定接近),那么你可以说这个数字属于那一列。

另一种方法是在每个页面写入后截取“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坐标来决定每个数字属于哪个列。

数字之间没有空格的原因是您必须设置分隔符string。

我希望这对你或其他可能尝试类似事情的人有用。

我曾经使用过许多工具从pdf文件中提取表格,但是这对我没有任何作用。

所以我已经实现了我自己的algorithm(其名称是traprange )来parsingpdf文件中的表格数据。

以下是一些示例pdf文件和结果:

  1. input文件: sample-1.pdf ,结果: sample-1.html
  2. input文件: sample-4.pdf ,结果: sample-4.html

访问我的项目页面在traprange 。

您可以在PDFBox中按区域提取文本。 如果您使用的是Maven,请参阅pdfbox-examples工件中的ExtractByArea.java示例文件。 一个片段看起来像

  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)并打印出每个字符的坐标,并找出它们在文档中的位置。

但是有一个更简单的方法,至less如果你在Mac上。 在预览中打开PDF,⌘I显示检查器,select裁剪选项卡并确保单位在点中,从工具菜单中select矩形select,然后select感兴趣的区域。 如果你select一个区域,检查员将显示你的坐标,你可以把它们放到Rectangle构造函数参数中。 您只需要使用第一种方法确认原点在哪里。

我在parsingpdftotext工具生成的文本文件(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); } 

从PDF中提取数据肯定会遇到问题。 文件是通过某种自动过程创build的吗? 如果是这样的话,您可以考虑将PDF转换为未压缩的PostScript(尝试pdf2ps)并查看PostScript是否包含某种可以利用的常规模式。

我在阅读PDF格式的文件时遇到同样的问题。 在使用PDFBox进行常规parsing之后,每行都用逗号作为分隔符提取…失去了柱状位置。 为了解决这个问题,我使用了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行,等等…

有PDFLayoutTextStripper被devise来保持数据的格式。

从自述文件:

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

http://swftools.org/这些家伙有一个pdf2swf组件。; 他们也能够显示表格。 他们也在给源。 所以你可以检查出来。

如果PDF文件具有使用pdfbox 2.0.6的“Only Rectangular table”,这工作正常。 不能与其他任何表格一起使用矩形表格。

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

您可以使用PDFBox的PDFTableStripperByArea类从文档的特定区域提取文本。 您可以通过识别表格的每个单元格的区域来进行构build。 这不是开箱即用的,但DrawPrintTextLocations类示例演示了如何parsing文档中单个字符的边界框(parsingstring或段落的边界框会很好,但我没有看到支持在PDFBox为此 – 看到这个问题 )。 您可以使用此方法将所有触摸边界框组合在一起以识别表格的不同单元格。 一种方法是维护一组Rectangle2D区域,然后为每个parsing的字符find字符的边界框,如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); 

然后可以将这些区域传递给PDFTableStripperByArea

你也可以进一步分离出这些区域的水平和垂直分量,从而推断出所有表格单元格的区域,而不pipe是否保存任何内容。

我有原因执行这些步骤,并最终使用PDFBox编写我自己的PDFTableStripper类。 我已经在GitHub上分享了我的代码。 main方法给出了一个如何使用类的例子:

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

我不熟悉PDFBox,但你可以尝试看看itext 。 即使主页上说PDF生成,你也可以做PDF操作和提取。 看看它是否适合你的用例。

如何打印图像和做OCR呢?

听起来非常无效,但它实际上是PDF的目的,使文本无法访问,你必须做你必须做的。

为了从pdf文件中读取表格的内容,您只需要使用任何API(我使用iText的PdfTextExtracter.getTextFromPage())将pdf文件转换为文本文件,然后通过java程序读取该文本文件..读完后,主要任务完成..你必须过滤你的需要的数据。 你可以通过不断使用String类的拆分方法来做到这一点,直到你find你的intrest的logging。这里是我的代码,我已经通过PDF文件提取logging的一部分,并将其写入一个.CSV文件.. PDF文件是.. http://www.cea.nic.in/reports/monthly/generation_rep/actual/jan13/opm_02.pdf

码:-

 public static void genrateCsvMonth_Region(String pdfpath, String csvpath) { try { String line = null; // Appending Header in CSV file... BufferedWriter writer1 = new BufferedWriter(new FileWriter(csvpath, true)); writer1.close(); // Checking whether file is empty or not.. BufferedReader br = new BufferedReader(new FileReader(csvpath)); if ((line = br.readLine()) == null) { BufferedWriter writer = new BufferedWriter(new FileWriter( csvpath, true)); writer.append("REGION,"); writer.append("YEAR,"); writer.append("MONTH,"); writer.append("THERMAL,"); writer.append("NUCLEAR,"); writer.append("HYDRO,"); writer.append("TOTAL\n"); writer.close(); } // Reading the pdf file.. PdfReader reader = new PdfReader(pdfpath); BufferedWriter writer = new BufferedWriter(new FileWriter(csvpath, true)); // Extracting records from page into String.. String page = PdfTextExtractor.getTextFromPage(reader, 1); // Extracting month and Year from String.. String period1[] = page.split("PEROID"); String period2[] = period1[0].split(":"); String month[] = period2[1].split("-"); String period3[] = month[1].split("ENERGY"); String year[] = period3[0].split("VIS"); // Extracting Northen region String northen[] = page.split("NORTHEN REGION"); String nthermal1[] = northen[0].split("THERMAL"); String nthermal2[] = nthermal1[1].split(" "); String nnuclear1[] = northen[0].split("NUCLEAR"); String nnuclear2[] = nnuclear1[1].split(" "); String nhydro1[] = northen[0].split("HYDRO"); String nhydro2[] = nhydro1[1].split(" "); String ntotal1[] = northen[0].split("TOTAL"); String ntotal2[] = ntotal1[1].split(" "); // Appending filtered data into CSV file.. writer.append("NORTHEN" + ","); writer.append(year[0] + ","); writer.append(month[0] + ","); writer.append(nthermal2[4] + ","); writer.append(nnuclear2[4] + ","); writer.append(nhydro2[4] + ","); writer.append(ntotal2[4] + "\n"); // Extracting Western region String western[] = page.split("WESTERN"); String wthermal1[] = western[1].split("THERMAL"); String wthermal2[] = wthermal1[1].split(" "); String wnuclear1[] = western[1].split("NUCLEAR"); String wnuclear2[] = wnuclear1[1].split(" "); String whydro1[] = western[1].split("HYDRO"); String whydro2[] = whydro1[1].split(" "); String wtotal1[] = western[1].split("TOTAL"); String wtotal2[] = wtotal1[1].split(" "); // Appending filtered data into CSV file.. writer.append("WESTERN" + ","); writer.append(year[0] + ","); writer.append(month[0] + ","); writer.append(wthermal2[4] + ","); writer.append(wnuclear2[4] + ","); writer.append(whydro2[4] + ","); writer.append(wtotal2[4] + "\n"); // Extracting Southern Region String southern[] = page.split("SOUTHERN"); String sthermal1[] = southern[1].split("THERMAL"); String sthermal2[] = sthermal1[1].split(" "); String snuclear1[] = southern[1].split("NUCLEAR"); String snuclear2[] = snuclear1[1].split(" "); String shydro1[] = southern[1].split("HYDRO"); String shydro2[] = shydro1[1].split(" "); String stotal1[] = southern[1].split("TOTAL"); String stotal2[] = stotal1[1].split(" "); // Appending filtered data into CSV file.. writer.append("SOUTHERN" + ","); writer.append(year[0] + ","); writer.append(month[0] + ","); writer.append(sthermal2[4] + ","); writer.append(snuclear2[4] + ","); writer.append(shydro2[4] + ","); writer.append(stotal2[4] + "\n"); // Extracting eastern region String eastern[] = page.split("EASTERN"); String ethermal1[] = eastern[1].split("THERMAL"); String ethermal2[] = ethermal1[1].split(" "); String ehydro1[] = eastern[1].split("HYDRO"); String ehydro2[] = ehydro1[1].split(" "); String etotal1[] = eastern[1].split("TOTAL"); String etotal2[] = etotal1[1].split(" "); // Appending filtered data into CSV file.. writer.append("EASTERN" + ","); writer.append(year[0] + ","); writer.append(month[0] + ","); writer.append(ethermal2[4] + ","); writer.append(" " + ","); writer.append(ehydro2[4] + ","); writer.append(etotal2[4] + "\n"); // Extracting northernEastern region String neestern[] = page.split("NORTH"); String nethermal1[] = neestern[2].split("THERMAL"); String nethermal2[] = nethermal1[1].split(" "); String nehydro1[] = neestern[2].split("HYDRO"); String nehydro2[] = nehydro1[1].split(" "); String netotal1[] = neestern[2].split("TOTAL"); String netotal2[] = netotal1[1].split(" "); writer.append("NORTH EASTERN" + ","); writer.append(year[0] + ","); writer.append(month[0] + ","); writer.append(nethermal2[4] + ","); writer.append(" " + ","); writer.append(nehydro2[4] + ","); writer.append(netotal2[4] + "\n"); writer.close(); } catch (IOException ioe) { ioe.printStackTrace(); } }