如何从PDF文件中提取文本和文本坐标?

58

我希望使用PDFMiner从PDF文件中提取所有文本框和其坐标信息。

许多其他的Stack Overflow帖子介绍如何按顺序提取所有文本,但是如何在其中间步骤中获取文本和其位置信息呢?

给定一个PDF文件,输出的结果应该类似于:

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

请参考 https://dev59.com/Nl8e5IYBdhLWcg3w6NyW,这是一个几个月后发布的重复贴。 - Mark Amery
4个回答

59

这是一个可复制粘贴的示例,列出了PDF中每个文本块的左上角位置。我认为这个示例适用于不包含具有文本的“表单XObjects”的任何PDF:

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 (https://dev59.com/IGAh5IYBdhLWcg3wBfiM#22898159)和Matt Swain (https://dev59.com/Nl8e5IYBdhLWcg3w6NyW#25262470)的示例。我从这些先前的示例中进行了一些更改:
  • 我使用PDFPage.get_pages(),它是创建文档的速记形式,检查is_extractable并将其传递给PDFPage.create_pages()
  • 我不费力处理LTFigure,因为PDFMiner目前无法清洁地处理其中的文本。
LAParams允许您设置一些参数,以控制PDFMiner如何通过魔法将PDF中的单个字符组合成行和文本框。如果您惊讶于这样的分组需要发生的事情,那么在pdf2txt文档中是有道理的:

在实际的PDF文件中,文本部分可能会根据创作软件在运行过程中被分成几个块。因此,文本提取需要拼接文本块。

LAParams的参数大多数都没有文档,但是你可以在源代码中看到它们in the source code或在Python shell中调用help(LAParams)。一些参数的含义在https://pdfminer-docs.readthedocs.io/pdfminer_index.html#pdf2txt-py上给出,因为它们也可以作为命令行参数传递给pdf2text

上面的layout对象是一个LTPage,是一个“布局对象”的可迭代对象。每个布局对象可以是以下类型之一...

  • LTTextBox
  • LTFigure
  • LTImage
  • LTLine
  • LTRect

... 或它们的子类。(特别地,你的文本框可能都是LTTextBoxHorizontal。)

从文档中可以看到LTPage的结构更详细的信息:

Tree diagram of the structure of an <code>LTPage</code>. Of relevance to this answer: it shows that an <code>LTPage</code> contains the 5 types listed above, and that an <code>LTTextBox</code> contains <code>LTTextLine</code>s plus unspecified other stuff, and that an <code>LTTextLine</code> contains <code>LTChar</code>s, <code>LTAnno</code>s, <code>LTText</code>s, and unspecified other stuff.

每种类型都有一个.bbox属性,其中包含一个(x0, y0, x1, y1)元组,分别包含对象左侧、底部、右侧和顶部的坐标。 y坐标是相对于页面底部的距离。 如果您更喜欢使用从上到下的Y轴进行工作,则可以从页面的 .mediabox 高度中减去它们。
x0, y0_orig, x1, y1_orig = some_lobj.bbox
y0 = page.mediabox[3] - y1_orig
y1 = page.mediabox[3] - y0_orig

除了外,LTTextBox还有一个.get_text()方法,如上所示,返回其文本内容作为字符串。请注意,每个LTTextBox都是LTChar(PDF明确绘制的字符,具有)和LTAnno(PDFMiner根据字符被长距离分开绘制的字符串表示形式添加的额外空格;这些没有)的集合。

本答案开头的代码示例将这两个属性组合在一起,显示每个文本块的坐标。

最后值得注意的是,与上面引用的其他Stack Overflow答案不同,我不会递归到LTFigure中。虽然LTFigure可以包含文本,但PDFMiner似乎无法将该文本分组为LTTextBoxes(您可以在https://stackoverflow.com/a/27104504/1709587的示例PDF上尝试自己),而是产生一个直接包含LTChar对象的。理论上,您可以找出如何将它们组合成一个字符串,但PDFMiner(截至20181108版本)不能为您完成此操作。

希望您需要解析的PDF文件不使用带有文本的Form XObjects,因此这个警告对您不适用。


1
嗨,我有一个发票格式的PDF文件。我可以提供要提取的文本位置,然后它会提取这些文本字段吗? - Baktaawar
1
如果我已经有坐标并想使用它来获取文本,那么这个库是否有用? - m9m9m

50

在最终输出中,换行符被转换为下划线。这是我找到的最简工作解决方案。

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)

3
我留下了自己的答案,对其进行了一些调整。你在此处创建的第一个“device”没有使用,并且设置解析的初始混乱可以通过“get_pages”缩短。我特别想知道:您是否曾经遇到过递归进入“LTFigure”起作用的情况? 我自己的实验表明,其中的文本不会被PDFMiner分组为文本框对象,因此您在此处对它们进行的递归永远不会起作用。 - Mark Amery
2
从Google进入的任何人几乎肯定希望在底部找到最新的答案,这非常优雅。https://dev59.com/IGAh5IYBdhLWcg3wBfiM#69151177 - Matt

24

申明一下,我是pdfminer.six的维护者之一,这是Python 3的pdfminer社区版本。

现在,pdfminer.six有多个API可以从PDF中提取文本和信息。为了以编程方式提取信息,建议使用extract_pages()方法。它允许您检查页面上的所有元素,并按照布局算法创建的有意义的层次结构进行排序。

以下示例展示了一种Pythonic的方法来显示层次结构中的所有元素。它使用pdfminer.six示例目录中的simple1.pdf文件。

from pathlib import Path
from typing import Iterable, Any

from pdfminer.high_level import extract_pages


def show_ltitem_hierarchy(o: Any, 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_indented_name(o: Any, depth: int) -> str:
    """Indented name of LTItem"""
    return '  ' * depth + o.__class__.__name__


def get_optional_bbox(o: Any) -> 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 ''


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


path = Path('~/Downloads/simple1.pdf').expanduser()

pages = extract_pages(path)
show_ltitem_hierarchy(pages)

输出显示了层次结构中的不同元素、每个元素的边界框以及该元素所包含的文本。

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                  

(类似的答案请参考这里这里这里,我会尝试保持它们同步。)


8
太美了,太神奇了! - Matt
未来是否有可能支持pdfminer将LTFigure中的LTChar分组为LTTextBox - Noxeus
PDF的链接不正确。它把我带到了另一个网页。您能否友好地发布正确的PDF链接?谢谢。 - Chadee Fouad
1
完成。现在它直接链接到PDF文件。 - Pieter
谢谢您提供这段代码,它真的帮了我大忙!不知道是否可以调整一下以显示页面编号?我的PDF文件有多页,每页都有类似的项目和坐标,因此很难知道文本属于哪一页,因为所有页面都具有相同的坐标。我只对LTTextBoxHorizontal感兴趣...非常感谢! :-) - Chadee Fouad

1

使用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

很不幸,对我来说没有起作用。 - Chadee Fouad

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