【问题标题】:PDFBox : Maintaining PDF structure when extracting textPDFBox : 提取文本时保持 PDF 结构
【发布时间】:2015-12-03 02:12:45
【问题描述】:

我正在尝试从充满表格的 PDF 中提取文本。 在某些情况下,一列是空的。 当我从 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 的完整结构?

非常感谢。

【问题讨论】:

  • 尝试使用像 tabula java 这样的工具,它位于 PDFBox 之上。 PDFBox 不会尝试识别表格。
  • Leor,如果您对 PDFTextStripper 的变体感兴趣,它试图在 PDF 中有很大空白的地方插入额外的空格,我将复制 the answer I once gave to a meanwhile deleted question变体。
  • @mkl 您的解决方案可能会有所帮助。如果添加的额外空格始终相同(就字符数而言),它可以完成这项工作。

标签: java pdfbox


【解决方案1】:

(这原本是the answer (dated Feb 6 '15) to another question,OP 删除了包括所有答案在内的所有答案。由于年代久远,答案中的代码仍然基于 PDFBox 1.8.x,因此可能需要进行一些更改才能使其运行使用 PDFBox 2.0.x。)

在 cmets 中,OP 对解决方案表现出兴趣扩展 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 中的值很感兴趣。

它基本上模拟了this answer 中 iText 的类似解决方案。但结果略有不同,因为 iText 文本提取转发文本块,而 PDFBox 文本提取转发单个字符。

请注意,这只是一个概念验证。它尤其不考虑任何旋转

【讨论】:

  • 您的解决方案运行良好。它需要进行一些转换以匹配我使用的 PDBox 版本,但是,第一次运行很有希望。结构几乎与原始 PDF 相同。如果有更好的解决方案,我将使用此解决方案。非常感谢
  • 这个使用LayoutTextStripper 的解决方案对我的应用程序很有用。但是,有时我会收到诸如“人的姓名和地址”之类的文本作为“人的姓名和地址”——单词之间缺少一些单空格。我正在使用 PDFBox 2.0.13。我该怎么做才能正确获得它(我第一次使用 PDFBox,我对使用版本 2 运行的代码所做的更改可能会导致)?感谢您的任何建议。
  • 好的,我找到了适用于 PDFBox 2.x 的 PDFLayoutTextStripper 的工作版本。
  • 正如答案中提到的,它提供了一个概念验证,所以一些细节可能仍然很粗糙。我假设更改(降低)fixedCharWidth 值可能有助于上面的代码。
猜你喜欢
  • 1970-01-01
  • 2012-12-30
  • 2014-12-21
  • 2018-12-17
  • 1970-01-01
  • 1970-01-01
  • 2015-11-19
  • 2018-09-12
相关资源
最近更新 更多