PDFBox:在提取文本时保持PDF结构

3
我正在尝试从一个充满表格的PDF文件中提取文本。在某些情况下,一列是空的。当我从PDF中提取文本时,空列被跳过并替换为空格,因此,我的正则表达式无法确定该位置上是否有没有信息的列。
更好地理解这个问题,请看下面的图片:

Image of PDF source and extracted text

我们可以看到,从PDF中提取的文本没有保留列格式。
我提取PDF文本的代码示例:
PDFTextStripper reader = new PDFTextStripper();
            reader.setSortByPosition(true);
            reader.setStartPage(page);
            reader.setEndPage(page);
            String st = reader.getText(document);
            List<String> lines = Arrays.asList(st.split(System.getProperty("line.separator")));

如何在从PDF中提取文本时保持原始PDF的完整结构?
非常感谢。

1
尝试使用像Tabula Java这样的工具,它是建立在PDFBox之上的。PDFBox不会尝试识别表格。 - Tilman Hausherr
Leor,如果你对PDFTextStripper的变体感兴趣,它会尝试在PDF中存在大间隙的地方插入额外的空格,我将复制我曾经回答过的一个已被删除的问题,其中包含这样的变体。 - mkl
@mkl 您的解决方案可能会有所帮助。如果添加的额外空格始终相同(在字符数方面),它可以完成工作。 - Leor
1个回答

2

(这原本是另一个问题的答案(日期为2015年2月6日),该问题的提问者删除了包括所有答案。由于时代久远,答案中的代码仍基于PDFBox 1.8.x,因此可能需要一些更改才能使其运行于PDFBox 2.0.x。)

在评论中,提问者表达了对扩展PDFBox PDFTextStripper 以返回尝试反映PDF文件布局的文本行的解决方案感兴趣,这可能有助于解决该问题。

该类的概念验证如下:

public class LayoutTextStripper extends PDFTextStripper
{
    public LayoutTextStripper() throws IOException
    {
        super();
    }

    @Override
    protected void startPage(PDPage page) throws IOException
    {
        super.startPage(page);
        cropBox = page.findCropBox();
        pageLeft = cropBox.getLowerLeftX();
        beginLine();
    }

    @Override
    protected void writeString(String text, List<TextPosition> textPositions) throws IOException
    {
        float recentEnd = 0;
        for (TextPosition textPosition: textPositions)
        {
            String textHere = textPosition.getCharacter();
            if (textHere.trim().length() == 0)
                continue;

            float start = textPosition.getTextPos().getXPosition();
            boolean spacePresent = endsWithWS | textHere.startsWith(" ");

            if (needsWS | spacePresent | Math.abs(start - recentEnd) > 1)
            {
                int spacesToInsert = insertSpaces(chars, start, needsWS & !spacePresent);

                for (; spacesToInsert > 0; spacesToInsert--)
                {
                    writeString(" ");
                    chars++;
                }
            }

            writeString(textHere);
            chars += textHere.length();

            needsWS = false;
            endsWithWS = textHere.endsWith(" ");
            try
            {
                recentEnd = getEndX(textPosition);
            }
            catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e)
            {
                throw new IOException("Failure retrieving endX of TextPosition", e);
            }
        }
    }

    @Override
    protected void writeLineSeparator() throws IOException
    {
        super.writeLineSeparator();
        beginLine();
    }

    @Override
    protected void writeWordSeparator() throws IOException
    {
        needsWS = true;
    }

    void beginLine()
    {
        endsWithWS = true;
        needsWS = false;
        chars = 0;
    }

    int insertSpaces(int charsInLineAlready, float chunkStart, boolean spaceRequired)
    {
        int indexNow = charsInLineAlready;
        int indexToBe = (int)((chunkStart - pageLeft) / fixedCharWidth);
        int spacesToInsert = indexToBe - indexNow;
        if (spacesToInsert < 1 && spaceRequired)
            spacesToInsert = 1;

        return spacesToInsert;
    }

    float getEndX(TextPosition textPosition) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException
    {
        Field field = textPosition.getClass().getDeclaredField("endX");
        field.setAccessible(true);
        return field.getFloat(textPosition);
    }

    public float fixedCharWidth = 3;

    boolean endsWithWS = true;
    boolean needsWS = false;
    int chars = 0;

    PDRectangle cropBox = null;
    float pageLeft = 0;
}

它的使用方式如下:
PDDocument document = PDDocument.load(PDF);

LayoutTextStripper stripper = new LayoutTextStripper();
stripper.setSortByPosition(true);
stripper.fixedCharWidth = charWidth; // e.g. 5

String text = stripper.getText(document);

“fixedCharWidth”是假定的字符宽度。根据所涉及PDF中的书写,可能需要不同的值。在我的示例文档中,3到6的值比较有用。
它本质上模拟了iText中类似的解决方案(请参见this answer)。然而,结果有些不同,因为iText文本提取将文本块向前移动,而PDFBox文本提取将单个字符向前移动。
请注意,这只是一个概念验证。它尤其没有考虑任何旋转。

你的解决方案非常有效。需要进行一些转换以匹配我所使用的PDBox版本,但第一次运行很有前途。 结构与原始PDF几乎完全相同。 如果没有更好的解决方案,我将使用这个解决方案。非常感谢。 - Leor
使用LayoutTextStripper的解决方案对我的应用程序很有用。但是,有时我会得到像“人的姓名和地址”这样的文本,而它会变成“人的姓名和地址” - 一些单个空格在单词之间丢失了。我正在使用PDFBox 2.0.13。我该怎么做才能正确地获取它(我第一次使用PDFBox,并且我对代码进行的更改以使用版本2可能会导致问题)?感谢任何建议。 - prasad_
1
好的,我找到了适用于PDFBox 2.x的PDFLayoutTextStripper的可工作版本。 - prasad_
正如答案中所提到的,它展示了一个“概念验证”,因此某些细节可能仍然不够完善。我认为更改(降低)上述代码中的fixedCharWidth值可能会有所帮助。 - mkl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接