我希望使用PDFMiner从PDF文件中提取所有文本框和其坐标信息。
许多其他的Stack Overflow帖子介绍如何按顺序提取所有文本,但是如何在其中间步骤中获取文本和其位置信息呢?
给定一个PDF文件,输出的结果应该类似于:
489, 41, "Signature"
500, 52, "b"
630, 202, "a_g_i_r"
这是一个可复制粘贴的示例,列出了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))
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
的结构更详细的信息:
.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似乎无法将该文本分组为LTTextBox
es(您可以在https://stackoverflow.com/a/27104504/1709587的示例PDF上尝试自己),而是产生一个直接包含LTChar
对象的。理论上,您可以找出如何将它们组合成一个字符串,但PDFMiner(截至20181108版本)不能为您完成此操作。
希望您需要解析的PDF文件不使用带有文本的Form XObjects,因此这个警告对您不适用。
在最终输出中,换行符被转换为下划线。这是我找到的最简工作解决方案。
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)
申明一下,我是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
LTFigure
中的LTChar
分组为LTTextBox
? - Noxeus使用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