我需要解析包含表格数据的PDF文件。我正在使用PDFBox提取文件文本以便稍后解析结果(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库,因此使用其他库的解决方案很好。我想要的是能够解析文件并知道每个解析的数字意味着什么。
答案 0 :(得分:18)
您需要设计一种算法来以可用的格式提取数据。无论您使用哪个PDF库,都需要执行此操作。字符和图形由一系列有状态的绘制操作绘制,即移动到屏幕上的这个位置并绘制字符'c'的字形。
我建议您扩展org.apache.pdfbox.pdfviewer.PDFPageDrawer
并覆盖strokePath
方法。从那里,您可以截取水平和垂直线段的绘制操作,并使用该信息确定表的列和行位置。然后简单的设置文本区域并确定在哪个区域中绘制哪些数字/字母/字符。由于您知道区域的布局,因此您将能够确定提取的文本属于哪个列。
此外,您在视觉上分开的文本之间可能没有空格的原因是,PDF经常不会绘制空格字符。而是更新文本矩阵并发出“移动”的绘图命令以绘制下一个字符和除最后一个字符之外的“空间宽度”。
祝你好运。答案 1 :(得分:12)
我曾使用过很多工具从pdf文件中提取表格,但它对我没用。
所以我实现了自己的算法(名称为traprange
)来解析pdf文件中的表格数据。
以下是一些示例pdf文件和结果:
访问我的项目页面traprange。
答案 2 :(得分:11)
您可以在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
构造函数参数。您只需要使用第一种方法确认原点的位置。
答案 3 :(得分:10)
我的答案可能为时已晚,但我认为这并不难。您可以扩展PDFTextStripper类并覆盖writePage()和processTextPosition(...)方法。在您的情况下,我假设列标题始终相同。这意味着您知道每个列标题的x坐标,并且可以将数字的x坐标与列标题的x坐标进行比较。如果它们足够接近(你必须测试以确定接近程度),那么你可以说该数字属于该列。
另一种方法是在每个页面写入后拦截“charactersByArticle”Vector:
@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坐标以确定每个数字所属的列。
数字之间没有空格的原因是你必须设置单词分隔符字符串。
我希望这对你或其他可能尝试类似事情的人有用。
答案 4 :(得分:7)
PDFLayoutTextStripper旨在保留数据格式。
来自自述文件:
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);
}
}
答案 5 :(得分:4)
我在解析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);
}
答案 6 :(得分:2)
从PDF中提取数据必然会遇到问题。文档是通过某种自动过程创建的吗?如果是这样,您可以考虑将PDF转换为未压缩的PostScript(尝试pdf2ps)并查看PostScript是否包含某种可以利用的常规模式。
答案 7 :(得分:2)
您可以使用PDFBox的PDFTextStripperByArea
类从文档的特定区域提取文本。您可以通过识别表格的每个单元格来构建此基础。这不是开箱即用的,但是示例DrawPrintTextLocations
类演示了如何解析文档中单个字符的边界框(解析字符串或段落的边界框会很棒,但我没有在PDFBox中看到了对此的支持 - 请参阅此question)。您可以使用此方法对所有触摸边界框进行分组,以识别表格的不同单元格。一种方法是维护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
类。我将我的代码分享为gist on 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));
}
}
}
}
答案 8 :(得分:2)
我在阅读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行,依此类推......
答案 9 :(得分:1)
尝试使用TabulaPDF(https://github.com/tabulapdf/tabula)。这是一个非常好的库,可以从PDF文件中提取表格内容。非常符合预期。
祝你好运。 :)
答案 10 :(得分:0)
http://swftools.org/这些家伙有一个pdf2swf组件。他们也可以显示表格。 他们也在提供消息来源。所以你可以检查一下。
答案 11 :(得分:0)
如果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;
}
}
答案 12 :(得分:0)
对于任何想做OP一样的事情(像我一样)的人,经过几天的研究Amazon Textract是最好的选择(如果您的免费套餐数量较少,就足够了)。
答案 13 :(得分:0)
我不需要使用PDFBox库,因此使用其他库的解决方案很好
卡米洛特和神剑
您可能想尝试Python库Camelot,这是Python的开源库。如果您不想编写代码,则可以使用围绕Camelot创建的Web界面Excalibur。您将文档“上载”到localhost Web服务器,然后从该localhost服务器“下载”结果。
以下是使用此python代码的示例:
import camelot
tables = camelot.read_pdf('foo.pdf', flavor="stream")
tables[0].to_csv('foo.csv')
输入是包含此表的pdf:
中的样本表没有为camelot提供帮助,它通过查看文本相对对齐来独立工作。结果返回到csv文件中:
按驼色从样品中提取的PDF表
可以添加“规则”以帮助camelot识别复杂表中的鱼片位置:
Excalibur中添加了规则。 Source
GitHub:
两个项目都处于活动状态。
Here是与其他软件(根据实际文档进行测试),Tabula,pdfplumber,pdftables,pdf-table-extract的比较。
我希望能够解析文件并知道每个解析后的数字的含义
您无法自动执行此操作,因为pdf并非语义结构。
书本与文档
Pdf“文档”从语义的角度来看是非结构化的(就像记事本文件一样),pdf文档给出了在何处打印文本片段的说明,该文本片段与同一节的其他片段无关,内容之间没有分隔(打印内容,以及这是标题,表格还是脚注的片段以及视觉表示(字体,位置等)。 Pdf是PostScript的扩展,它描述了Hello世界!这样的页面:
!PS
/Courier % font
20 selectfont % size
72 500 moveto % current location to print at
(Hello world!) show % add text fragment
showpage % print all on the page
(维基百科)。
可以想象,使用相同的指令,表格是什么样的。
我们可以说html不太清晰,但是有很大的不同:HTML以语义方式描述内容(标题,段落,列表,表标题,表格单元格...),并将CSS关联为可视形式,因此内容是完全可访问的。从这个意义上讲,html是sgml的简化后代,它施加了约束以允许进行数据处理:
标记应描述文档的结构和其他属性 而不是指定需要执行的处理,因为 它不太可能与未来的发展冲突。
与PostScript / Pdf相反。 SGML用于发布中。 Pdf不嵌入这种语义结构,它仅包含与纯字符串相关联的css等效项,这些字符串可能不是完整的单词或句子。 Pdf用于关闭的文档,现在用于所谓的workflow management。
在尝试了不确定性和尝试从pdf提取数据的困难之后,很明显pdf根本不是保留将来文档内容的解决方案(尽管Adobe从他们的对中获得了pdf standard )。
实际上保留得很好的是印刷形式,因为pdf在创建时就完全致力于此方面。 Pdf几乎和印刷书籍一样死了。
重用内容很重要时,您必须再次依靠手动重新输入数据,例如从印刷书籍中重新输入(可能尝试对它进行一些OCR)。越来越多的事实如此,因为许多pdf甚至阻止了复制粘贴的使用,在单词之间引入多个空格,或者在对Web进行一些“优化”时产生乱码。
当文档的内容(而不是其印刷表示形式)有价值时,则pdf不是正确的格式。甚至Adobe也无法通过pdf渲染完美地重新创建文档的来源。
因此,公开的数据绝不能以pdf格式发布,这限制了它们只能用于读取和打印(如果允许),并使重用变得更加困难或不可能。
答案 14 :(得分:0)
ObjectExtractor oe = new ObjectExtractor(document);
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm(); // Tabula algo.
Page page = oe.extract(1); // extract only the first page
for (int y = 0; y < sea.extract(page).size(); y++) {
System.out.println("table: " + y);
Table table = sea.extract(page).get(y);
for (int i = 0; i < table.getColCount(); i++) {
for (int x = 0; x < table.getRowCount(); x++) {
System.out.println("col:" + i + "/lin:x" + x + " >>" + table.getCell(x, i).getText());
}
}
}
答案 15 :(得分:0)
考虑使用 PDFTableStripper.class
该课程在 git 上可用: https://gist.github.com/beldaz/8ed6e7473bd228fcee8d4a3e4525be11#file-pdftablestripper-java-L1
答案 16 :(得分:-1)
要从pdf文件中读取表格内容,您只需使用任何API(我使用iText的PdfTextExtracter.getTextFromPage())将pdf文件转换为文本文件,然后通过以下方式读取该文本文件:你的java程序..现在看完之后主要任务就完成了......你必须过滤你需要的数据。你可以通过连续使用String类的split方法来实现它,直到找到你的intrest的记录。这里是我的代码,通过它我可以通过PDF文件提取记录的一部分并将其写入.CSV文件.. PDF文件的URL文件是.. 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();
}
}
答案 17 :(得分:-1)
如何打印到图像并对其进行OCR?
听起来非常无效,但实际上PDF的目的是让文字无法访问,你必须做你必须做的事。
答案 18 :(得分:-1)
我不熟悉PDFBox,但您可以尝试查看itext。即使主页说PDF生成,您也可以进行PDF操作和提取。看看它是否适合您的使用案例。