PDFBox: сохранение структуры PDF при извлечении текста

Я пытаюсь извлечь текст из PDF, который полон таблиц.
В некоторых случаях столбец пуст.
Когда я извлекаю текст из 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-файла при извлечении из него текста ?

Спасибо большое.

1 ответ

  1. (Первоначально это был ответ (датированный 6 ‘ 15 февраля) на другой вопрос, который OP удалил, включая все ответы. Из-за возраста код в ответе все еще был основан на PDFBox 1.8.x, поэтому некоторые изменения могут потребоваться для его запуска с PDFBox 2.0.икс.)

    В комментариях OP проявил интерес к решению расширить PDFBoxPDFTextStripper, чтобы вернуть текстовые строки, которые пытаются отразить макет файла 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 в этом ответе . Результаты немного отличаются, хотя, как iText извлечение текста вперед текстовые фрагменты и PDFBox извлечение текста вперед отдельных символов.

    Пожалуйста, имейте в виду, что это просто доказательство концепции. Это особенно не учитывает ротацию