Извлечение текста с помощью PdfMiner и столбцов слияния PyPDF2

Я пытаюсь проанализировать текст файла PDF с помощью pdfMiner, но извлеченный текст объединяется. Я использую файл pdf по следующей ссылке.

PDF-файл

Я хорошо работаю с любым типом вывода (файл/строка). Вот код, который возвращает извлеченный текст в виде строки для меня, но по какой-то причине столбцы объединены.

from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfinterp import PDFResourceManager, process_pdf
import StringIO

def convert_pdf(filename):
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    codec = 'utf-8'
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, codec=codec)

    fp = file(filename, 'rb')
    process_pdf(rsrcmgr, device, fp)
    fp.close()
    device.close()

    str = retstr.getvalue()
    retstr.close()
    return str

Я также пробовал PyPdf2, но столкнулся с той же проблемой. Вот пример кода для PyPDF2

from PyPDF2.pdf import PdfFileReader
import StringIO
import time

def getDataUsingPyPdf2(filename):
    pdf = PdfFileReader(open(filename, "rb"))
    content = ""

    for i in range(0, pdf.getNumPages()):
        print str(i)
        extractedText = pdf.getPage(i).extractText()
        content +=  extractedText + "\n"

    content = " ".join(content.replace("\xa0", " ").strip().split())
    return content.encode("ascii", "ignore")

Я также пробовал pdf2txt.py, но не смог отформатировать выход.


person user2151334    schedule 01.04.2013    source источник
comment
Должен ли этот первый блок кода читать retstr = StringIO.StringIO()?   -  person Stedy    schedule 01.04.2013
comment
Несколько столбцов действительно больно читать из PDF. В зависимости от того, что вы хотите, k2pdfopt создает изображения с каждой страницы.   -  person bobrobbob    schedule 01.04.2013
comment
PDF-файл, который вы хотели связать, больше не доступен.   -  person CharlesG    schedule 09.03.2020


Ответы (3)


Недавно я боролся с похожей проблемой, хотя структура моего pdf была немного проще.

PDFMiner использует классы, называемые «устройствами», для анализа страниц в pdf-файле. Базовым классом устройства является класс PDFPageAggregator, который просто анализирует текстовые поля в файле. Классы преобразователя, например. TextConverter, XMLConverter и HTMLConverter также выводят результат в файл (или в поток строк, как в вашем примере) и выполняют более сложный анализ содержимого.

Проблема с TextConverter (и PDFPageAggregator) заключается в том, что они недостаточно глубоко рекурсивно обращаются к структуре документа для правильного извлечения различных столбцов. Двум другим преобразователям требуется некоторая информация о структуре документа для целей отображения, поэтому они собирают более подробные данные. В вашем примере pdf оба упрощенных устройства только анализируют (примерно) все текстовое поле, содержащее столбцы, что делает невозможным (или, по крайней мере, очень сложным) правильное разделение разных строк. Решение этого, которое я нашел, работает очень хорошо, либо

  • Создайте новый класс, наследуемый от PDFPageAggregator, или
  • Используйте XMLConverter и проанализируйте полученный XML-документ, используя, например. Красивый суп

В обоих случаях вам придется объединить различные текстовые сегменты в строки, используя их координаты y ограничивающей рамки.

В случае нового класса устройств (я думаю, это более красноречиво) вам придется переопределить метод receive_layout, который вызывается для каждой страницы в процессе рендеринга. Затем этот метод рекурсивно анализирует элементы на каждой странице. Например, что-то вроде этого может помочь вам начать:

from pdfminer.pdfdocument import PDFDocument, PDFNoOutlines
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LTPage, LTChar, LTAnno, LAParams, LTTextBox, LTTextLine

class PDFPageDetailedAggregator(PDFPageAggregator):
    def __init__(self, rsrcmgr, pageno=1, laparams=None):
        PDFPageAggregator.__init__(self, rsrcmgr, pageno=pageno, laparams=laparams)
        self.rows = []
        self.page_number = 0
    def receive_layout(self, ltpage):        
        def render(item, page_number):
            if isinstance(item, LTPage) or isinstance(item, LTTextBox):
                for child in item:
                    render(child, page_number)
            elif isinstance(item, LTTextLine):
                child_str = ''
                for child in item:
                    if isinstance(child, (LTChar, LTAnno)):
                        child_str += child.get_text()
                child_str = ' '.join(child_str.split()).strip()
                if child_str:
                    row = (page_number, item.bbox[0], item.bbox[1], item.bbox[2], item.bbox[3], child_str) # bbox == (x1, y1, x2, y2)
                    self.rows.append(row)
                for child in item:
                    render(child, page_number)
            return
        render(ltpage, self.page_number)
        self.page_number += 1
        self.rows = sorted(self.rows, key = lambda x: (x[0], -x[2]))
        self.result = ltpage

В приведенном выше коде каждый найденный элемент LTTextLine хранится в упорядоченном списке кортежей, содержащих номер страницы, координаты ограничивающей рамки и текст, содержащийся в этом конкретном элементе. Затем вы должны сделать что-то похожее на это:

from pprint import pprint
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.layout import LAParams

fp = open('pdf_doc.pdf', 'rb')
parser = PDFParser(fp)
doc = PDFDocument(parser)
doc.initialize('password') # leave empty for no password

rsrcmgr = PDFResourceManager()
laparams = LAParams()
device = PDFPageDetailedAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device)

for page in PDFPage.create_pages(doc):
    interpreter.process_page(page)
    # receive the LTPage object for this page
    device.get_result()

pprint(device.rows)

Переменная device.rows содержит упорядоченный список, в котором все текстовые строки упорядочены по номерам страниц и координатам Y. Вы можете перебирать текстовые строки и группировать строки с одинаковыми координатами Y, чтобы формировать строки, сохранять данные столбца и т. д.

Я попытался проанализировать ваш PDF-файл, используя приведенный выше код, и столбцы в основном анализируются правильно. Однако некоторые столбцы расположены так близко друг к другу, что стандартная эвристика PDFMiner не может разделить их на отдельные элементы. Вероятно, вы можете обойти это, настроив параметр поля слова (флаг -W в инструменте командной строки pdf2text.py). В любом случае, вы можете прочитать (плохо документированный) PDFMiner API, а также просмотрите исходный код PDFMiner, который вы можете получить на github. (Увы, я не могу вставить ссылку, потому что у меня недостаточно точек репутации: '‹, но вы, надеюсь, можете найти правильный репозиторий в Google)

person lindblandro    schedule 04.10.2013

Я попробовал ваш первый блок кода и получил кучу результатов, которые выглядят так:

МНОГОЖИЛОЙ САДОВЫЙ КОМПЛЕКС 14945010314370 TO 372WILLOWRD W МНОГОЖИЛОЙ САДОВЫЙ КОМПЛЕКС 14945010314380 TO 384WILLOWRD W МНОГОЖИЛОЙ САДОВЫЙ КОМПЛЕКС 149450103141000 TO 1020D8WILLOWBROOKRD MULTIPUSE

Я предполагаю, что вы находитесь в том же положении, что и этот ответ, и что все пробелы используются для размещения слов в правильном месте, а не как фактические печатные символы пробела. Тот факт, что вы пытались использовать другие библиотеки PDF, заставляет меня думать, что это может быть проблемой, которую трудно проанализировать любой библиотеке PDF.

person Stedy    schedule 01.04.2013

Решение, предоставленное @hlindblo, дало довольно хорошие результаты. Чтобы дополнительно сгруппировать извлеченные текстовые фрагменты по страницам и абзацам, вот простые команды, которые я использовал.

from collections import OrderedDict
grouped_text = OrderedDict()
for p in range(1000): # max page nb is 1000
    grouped_text[p] = {}
for (page_nb, x_min, y_min, x_max, y_max, text) in device.rows:
    x_min = round(x_min)//10 # manipulate the level of aggregation --> x_min might be slitghly different
    try:
        grouped_text[page_nb][x_min]+= " " + text
    except:
        grouped_text[page_nb][x_min] = text
person CharlesG    schedule 09.03.2020