我最近遇到了类似的问题,尽管我的 pdf 结构稍微简单一些。
PDFMiner 使用称为“设备”的类来解析 pdf 文件中的页面。基本设备类是 PDFPageAggregator 类,它只是解析文件中的文本框。转换器类,例如TextConverter、XMLConverter 和 HTMLConverter 还将结果输出到文件中(或示例中的字符串流中),并对内容进行一些更详细的解析。
TextConverter(和 PDFPageAggregator)的问题在于,它们对文档结构的递归程度不够深,无法正确提取不同的列。另外两个转换器需要一些有关文档结构的信息以用于显示目的,因此它们收集更详细的数据。在您的示例 pdf 中,两个简单设备仅解析(粗略地)包含列的整个文本框,这使得不可能(或至少非常困难)正确分隔不同的行。我发现这个问题的解决方案效果很好,要么
- 创建一个继承自 PDFPageAggregator 的新类,或者
- 使用 XMLConverter 并使用例如解析生成的 XML 文档美丽汤
在这两种情况下,您都必须使用边界框 y 坐标将不同的文本段组合到行中。
对于新的设备类(我认为这更有说服力),您必须重写该方法receive_layout
在渲染过程中每个页面都会调用该 get 方法。然后该方法递归地解析每个页面中的元素。例如,类似这样的事情可能会帮助您开始:
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 启发式方法无法将它们分离成自己的元素。您可以通过调整字边距参数(命令行工具 pdf2text.py 中的 -W 标志)来解决这个问题。无论如何,您可能需要通读(记录不充分)PDFMiner API以及浏览 PDFMiner 的源代码,您可以从 github 获取该源代码。 (唉,我无法粘贴链接,因为我没有足够的代表点:'