Как извлечь текст и текстовые координаты из файла PDF?

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

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

Учитывая файл PDF, вывод должен выглядеть примерно так:

   489, 41,  "Signature"
   500, 52,  "b"
   630, 202, "a_g_i_r"

4 ответа

Решение

Новые строки преобразуются в подчеркивания в конечном выводе. Это минимальное рабочее решение, которое я нашел.

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfpage import PDFTextExtractionNotAllowed
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfdevice import PDFDevice
from pdfminer.layout import LAParams
from pdfminer.converter import PDFPageAggregator
import pdfminer

# Open a PDF file.
fp = open('/Users/me/Downloads/test.pdf', 'rb')

# Create a PDF parser object associated with the file object.
parser = PDFParser(fp)

# Create a PDF document object that stores the document structure.
# Password for initialization as 2nd parameter
document = PDFDocument(parser)

# Check if the document allows text extraction. If not, abort.
if not document.is_extractable:
    raise PDFTextExtractionNotAllowed

# Create a PDF resource manager object that stores shared resources.
rsrcmgr = PDFResourceManager()

# Create a PDF device object.
device = PDFDevice(rsrcmgr)

# BEGIN LAYOUT ANALYSIS
# Set parameters for analysis.
laparams = LAParams()

# Create a PDF page aggregator object.
device = PDFPageAggregator(rsrcmgr, laparams=laparams)

# Create a PDF interpreter object.
interpreter = PDFPageInterpreter(rsrcmgr, device)

def parse_obj(lt_objs):

    # loop over the object list
    for obj in lt_objs:

        # if it's a textbox, print text and location
        if isinstance(obj, pdfminer.layout.LTTextBoxHorizontal):
            print "%6d, %6d, %s" % (obj.bbox[0], obj.bbox[1], obj.get_text().replace('\n', '_'))

        # if it's a container, recurse
        elif isinstance(obj, pdfminer.layout.LTFigure):
            parse_obj(obj._objs)

# loop over all pages in the document
for page in PDFPage.create_pages(document):

    # read the page into a layout object
    interpreter.process_page(page)
    layout = device.get_result()

    # extract text from this object
    parse_obj(layout._objs)

Вот пример копирования и вставки, в котором перечислены верхние левые углы каждого блока текста в PDF, и который, я думаю, должен работать для любого PDF, который не содержит "Form XObjects", в которых есть текст:

from pdfminer.layout import LAParams, LTTextBox
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator

fp = open('yourpdf.pdf', 'rb')
rsrcmgr = PDFResourceManager()
laparams = LAParams()
device = PDFPageAggregator(rsrcmgr, laparams=laparams)
interpreter = PDFPageInterpreter(rsrcmgr, device)
pages = PDFPage.get_pages(fp)

for page in pages:
    print('Processing next page...')
    interpreter.process_page(page)
    layout = device.get_result()
    for lobj in layout:
        if isinstance(lobj, LTTextBox):
            x, y, text = lobj.bbox[0], lobj.bbox[3], lobj.get_text()
            print('At %r is text: %s' % ((x, y), text))

Приведенный выше код основан на примере анализа выполнения макетов в документах PDFMiner, а также на примерах pnj ( /questions/45024982/kak-izvlech-tekst-i-tekstovyie-koordinatyi-iz-fajla-pdf/45024994#45024994) и Мэтта Суэйна ( /questions/35684383/kak-poluchit-raspolozhenie-teksta-v-pdf-s-pdfminer/35684393#35684393). Есть несколько изменений, которые я сделал из этих предыдущих примеров:

  • я использую PDFPage.get_pages(), что является сокращением для создания документа, проверки его is_extractable и передать его PDFPage.create_pages()
  • Я не беспокоюсь об управлении LTFigure с, так как PDFMiner в настоящее время не может чисто обрабатывать текст внутри них в любом случае.

LAParams позволяет вам установить некоторые параметры, которые управляют тем, как отдельные символы в PDF волшебным образом группируются в строки и текстовые поля с помощью PDFMiner. Если вы удивлены, что такая группировка вообще необходима, это оправдано в документах pdf2txt:

В реальном PDF-файле текстовые части могут быть разбиты на несколько частей в середине процесса, в зависимости от программного обеспечения для создания. Следовательно, для извлечения текста необходимо объединить фрагменты текста.

LAParams Параметры, как и большинство PDFMiner, недокументированы, но вы можете увидеть их в исходном коде или по телефону help(LAParams) на вашей оболочке Python. Значение некоторых параметров приводится по адресу https://pdfminer-docs.readthedocs.io/pdfminer_index.html, поскольку их также можно передавать в качестве аргументов pdf2text в командной строке.

layout объект выше является LTPage, который является итерируемым из "объектов макета". Каждый из этих объектов макета может быть одного из следующих типов...

  • LTTextBox
  • LTFigure
  • LTImage
  • LTLine
  • LTRect

... или их подклассы. (В частности, ваши текстовые поля, вероятно, будут все LTTextBoxHorizontal с.)

Более подробная информация о структуре LTPage показано на этом изображении из документов:

Древовидная схема структуры <code>.bbox</code> свойство, которое содержит (<em>x0</em>, <em>y0</em>, <em>x1</em>, <em>y1</em>) кортеж, содержащий координаты слева, снизу, справа и сверху объекта соответственно. Y-координаты даны как расстояние от <em>нижней</em> части страницы. Если вам удобнее работать с осью Y, идущей сверху вниз, вы можете вычесть их из высоты страницы. <code>.mediabox</code>:</p><pre><code>x0, y0, x1, y1 = some_lobj.bbox
y0 = page.mediabox[3] - y1
y1 = page.mediabox[3] - y0
</code></pre><p>В дополнение к <code>bbox</code>, <code>LTTextBox</code> У них также есть <code>.get_text()</code> метод, показанный выше, который возвращает их текстовое содержимое в виде строки. Обратите внимание, что каждый <code>LTTextBox</code> это коллекция <code>LTChar</code> s (символы, явно нарисованные в PDF, с <code>bbox</code>) а также <code>LTAnno</code> s (дополнительные пробелы, которые PDFMiner добавляет к строковому представлению содержимого текстового поля на основе символов, отрисованных на большом расстоянии друг от друга; они не имеют <code>bbox</code>).</p><p>Пример кода в начале этого ответа объединил эти два свойства, чтобы показать координаты каждого блока текста.</p><p>Наконец, стоит отметить, что, в <em>отличие</em> от других ответов о переполнении стека, приведенных выше, я не буду возвращаться к <code>LTFigure</code> s. Хотя <code>LTFigure</code> s может содержать текст, PDFMiner не может сгруппировать этот текст в <code>LTTextBox</code> ES (вы можете попробовать себя на примере PDF с <a href=https://stackru.com/a/27104504/1709587) и вместо этого создает LTFigure который непосредственно содержит LTChar объекты. В принципе, вы могли бы выяснить, как объединить их в строку, но PDFMiner (начиная с версии 20181108) не может сделать это за вас.

Надеюсь, однако, что PDF-файлы, которые вам нужно проанализировать, не используют объекты XObject с текстом в них, и поэтому это предостережение не относится к вам.

Полное раскрытие информации, я являюсь одним из сопровождающих pdfminer.six . Это поддерживаемая сообществом версия pdfminer для python 3.

В настоящее время pdfminer.six имеет несколько API для извлечения текста и информации из PDF. В этом случае я бы посоветовал использовать высокоуровневую функцию extract_pages(). Это позволяет вам проверять все элементы на странице, упорядоченные в значимой иерархии, созданной алгоритмом макета.

Следующий пример представляет собой питонический способ отображения всех элементов иерархии. Он использует файл simple1.pdf из каталога примеров на pdfminer.six.

      import os
from typing import Iterable

from pdfminer.high_level import extract_pages
from pdfminer.layout import LTItem


def show_ltitem_hierarchy(o: LTItem, depth=0):
    """Show location and text of LTItem and all its descendants"""
    if depth == 0:
        print('element                        x1  y1  x2  y2   text')
        print('------------------------------ --- --- --- ---- -----')
    
    print(
        f'{get_indented_name(o, depth):<30.30s} '
        f'{get_optional_bbox(o)} '
        f'{get_optional_text(o)}'
    )

    if isinstance(o, Iterable):
        for i in o:
            show_ltitem_hierarchy(i, depth=depth + 1)


def get_optional_text(o: LTItem) -> str:
    """Text of LTItem if available, otherwise empty string"""
    if hasattr(o, 'get_text'):
        return o.get_text().strip()
    return ''


def get_indented_name(o: LTItem, depth: int) -> str:
    """Indented name of LTItem"""
    return '  ' * depth + o.__class__.__name__


def get_optional_bbox(o: LTItem) -> str:
    """Bounding box of LTItem if available, otherwise empty string"""
    if hasattr(o, 'bbox'):
        return ''.join(f'{i:<4.0f}' for i in o.bbox)
    return ''


file_path = '~/Downloads/simple1.pdf'
show_ltitem_hierarchy(extract_pages(os.path.expanduser(file_path)))

На выходе показаны различные элементы иерархии. Ограничивающая рамка для каждого. И текст, который содержит этот элемент.

      element                        x1  y1  x2  y2   text
------------------------------ --- --- --- ---- -----
generator                       
  LTPage                       0   0   612 792  
    LTTextBoxHorizontal        100 695 161 719  Hello
      LTTextLineHorizontal     100 695 161 719  Hello
        LTChar                 100 695 117 719  H
        LTChar                 117 695 131 719  e
        LTChar                 131 695 136 719  l
        LTChar                 136 695 141 719  l
        LTChar                 141 695 155 719  o
        LTChar                 155 695 161 719  
        LTAnno                  
    LTTextBoxHorizontal        261 695 324 719  World
      LTTextLineHorizontal     261 695 324 719  World
        LTChar                 261 695 284 719  W
        LTChar                 284 695 297 719  o
        LTChar                 297 695 305 719  r
        LTChar                 305 695 311 719  l
        LTChar                 311 695 324 719  d
        LTAnno                  
    LTTextBoxHorizontal        100 595 161 619  Hello
      LTTextLineHorizontal     100 595 161 619  Hello
        LTChar                 100 595 117 619  H
        LTChar                 117 595 131 619  e
        LTChar                 131 595 136 619  l
        LTChar                 136 595 141 619  l
        LTChar                 141 595 155 619  o
        LTChar                 155 595 161 619  
        LTAnno                  
    LTTextBoxHorizontal        261 595 324 619  World
      LTTextLineHorizontal     261 595 324 619  World
        LTChar                 261 595 284 619  W
        LTChar                 284 595 297 619  o
        LTChar                 297 595 305 619  r
        LTChar                 305 595 311 619  l
        LTChar                 311 595 324 619  d
        LTAnno                  
    LTTextBoxHorizontal        100 495 211 519  H e l l o
      LTTextLineHorizontal     100 495 211 519  H e l l o
        LTChar                 100 495 117 519  H
        LTAnno                  
        LTChar                 127 495 141 519  e
        LTAnno                  
        LTChar                 151 495 156 519  l
        LTAnno                  
        LTChar                 166 495 171 519  l
        LTAnno                  
        LTChar                 181 495 195 519  o
        LTAnno                  
        LTChar                 205 495 211 519  
        LTAnno                  
    LTTextBoxHorizontal        321 495 424 519  W o r l d
      LTTextLineHorizontal     321 495 424 519  W o r l d
        LTChar                 321 495 344 519  W
        LTAnno                  
        LTChar                 354 495 367 519  o
        LTAnno                  
        LTChar                 377 495 385 519  r
        LTAnno                  
        LTChar                 395 495 401 519  l
        LTAnno                  
        LTChar                 411 495 424 519  d
        LTAnno                  
    LTTextBoxHorizontal        100 395 211 419  H e l l o
      LTTextLineHorizontal     100 395 211 419  H e l l o
        LTChar                 100 395 117 419  H
        LTAnno                  
        LTChar                 127 395 141 419  e
        LTAnno                  
        LTChar                 151 395 156 419  l
        LTAnno                  
        LTChar                 166 395 171 419  l
        LTAnno                  
        LTChar                 181 395 195 419  o
        LTAnno                  
        LTChar                 205 395 211 419  
        LTAnno                  
    LTTextBoxHorizontal        321 395 424 419  W o r l d
      LTTextLineHorizontal     321 395 424 419  W o r l d
        LTChar                 321 395 344 419  W
        LTAnno                  
        LTChar                 354 395 367 419  o
        LTAnno                  
        LTChar                 377 395 385 419  r
        LTAnno                  
        LTChar                 395 395 401 419  l
        LTAnno                  
        LTChar                 410 395 424 419  d
        LTAnno                  

Это легко работает с pymupdf https://pymupdf.readthedocs.io/en/latest/app1.html.

      import fitz
with fitz.open(path_to_pdf_file) as document:
    words_dict = {}
    for page_number, page in enumerate(document):
        words = page.get_text("words")
        words_dict[page_number] = words
Другие вопросы по тегам