使用 JQuery 或 XPath 选择器的简洁友好的 PDF 抓取工具。
项目描述
使用 JQuery 或 XPath 语法进行简洁、友好的 PDF 抓取。
PDFQuery 是 pdfminer、lxml 和 pyquery 的轻量级包装器。它旨在以尽可能少的代码可靠地从 PDF 集中提取数据。
<nav class="contents" id="table-of-contents">目录
安装
easy_install pdfquery或pip install pdfquery。
快速开始
基本思想是将 PDF 文档转换为元素树,这样我们就可以使用 pyquery 找到具有类似 JQuery 的选择器的项目。假设我们试图从一组 PDF 中提取一个名字,但我们只知道它出现在每个 PDF 中“您的名字和首字母”的下方:
>>> pdf = pdfquery.PDFQuery("tests/samples/IRS_1040A.pdf")
>>> pdf.load()
>>> label = pdf.pq('LTTextLineHorizontal:contains("Your first name and initial")')
>>> left_corner = float(label.attr('x0'))
>>> bottom_corner = float(label.attr('y0'))
>>> name = pdf.pq('LTTextLineHorizontal:in_bbox("%s, %s, %s, %s")' % (left_corner, bottom_corner-30, left_corner+150, bottom_corner)).text()
>>> name
'John E.'
请注意,我们不必知道名称在页面上的什么位置,或者它在哪个页面上,或者 PDF 如何在内部存储它。
性能说明:对 pdf.load() 的初始调用运行非常缓慢,因为底层 pdfminer 库必须将页面上的每个元素与其他每个元素进行比较。请参阅缓存部分以避免在后续运行中出现这种情况。
现在让我们一次提取并格式化一堆数据:
>>> pdf = pdfquery.PDFQuery("tests/samples/IRS_1040A.pdf")
>>> pdf.extract( [
('with_parent','LTPage[pageid=1]'),
('with_formatter', 'text'),
('last_name', 'LTTextLineHorizontal:in_bbox("315,680,395,700")'),
('spouse', 'LTTextLineHorizontal:in_bbox("170,650,220,680")'),
('with_parent','LTPage[pageid=2]'),
('oath', 'LTTextLineHorizontal:contains("perjury")', lambda match: match.text()[:30]+"..."),
('year', 'LTTextLineHorizontal:contains("Form 1040A (")', lambda match: int(match.text()[-5:-1]))
])
结果:
{'last_name': 'Michaels',
'spouse': 'Susan R.',
'year': 2007,
'oath': 'Under penalties of perjury, I ...',}
用法
数据模型
PDFQuery 的工作原理是将 PDF 作为 pdfminer 布局加载,使用 lxml.etree 将布局转换为 etree,然后应用 pyquery 包装器。所有三个底层库都是公开的,因此您可以使用它们的任何接口来获取您想要的数据。
首先 pdfminer 打开文档并读取其布局。您可以在pdf.doc访问 pdfminer 文档:
>>> pdf = pdfquery.PDFQuery("tests/samples/IRS_1040A.pdf")
>>> pdf.doc
<pdfminer.pdfparser.PDFDocument object at 0xd95c90>
>>> pdf.doc.catalog # fetch attribute of underlying pdfminer document
{'JT': <PDFObjRef:14>, 'PageLabels': <PDFObjRef:10>, 'Type': /Catalog, 'Pages': <PDFObjRef:12>, 'Metadata': <PDFObjRef:13>}
接下来,布局变成带有 pyquery 包装器的 lxml.etree。在您调用pdf.load()(迄今为止该过程中最昂贵的操作)之后,您可以访问 pdf.tree 的etree和 pdf.pq 的pyquery包装器:
>>> pdf.load()
>>> pdf.tree
<lxml.etree._ElementTree object at 0x106a285f0>
>>> pdf.tree.write("test2.xml", pretty_print=True, encoding="utf-8")
>>> pdf.tree.xpath('//*/LTPage')
[<Element LTPage at 0x994cb0>, <Element LTPage at 0x994a58>]
>>> pdf.pq('LTPage[pageid=1] :contains("Your first name")')
[<LTTextLineHorizontal>]
如果您只使用所需的页码调用load() ,您将节省一些时间和内存。例如:
>>> pdf.load(0, 2, 3, range(4,8))
性能说明:对 pdf.load() 的初始调用运行非常缓慢,因为底层 pdfminer 库必须将页面上的每个元素与其他每个元素进行比较。请参阅缓存部分以避免在后续运行中出现这种情况。
在底层,pdf.tree 基本上是由 pdfminer.pdfinterp 生成的布局树的 XML 表示。默认情况下,树被处理以组合单个字符节点,删除额外的空格,并在空间上对树进行排序。您始终可以从 xpath 或 pyquery 获取的元素中返回原始 pdfminer 布局对象:
>>> pdf.pq(':contains("Your first name and initial")')[0].layout
<LTTextLineHorizontal 143.651,714.694,213.083,721.661 u'Your first name and initial\n'>
找到你想要的
PDF 内部是混乱的,因此通常不会像使用 HTML 那样根据文档结构或元素类来查找内容。相反,最可靠的选择器是页面上的静态标签,您可以通过搜索其文本内容和页面上的物理位置来找到它们。PDF 坐标从左下角开始以点(72 到英寸)给出。PDFMiner(以及 PDFQuery)根据边界框或 bbox 来描述页面位置。一个bbox由四个坐标组成:左下角的X和Y,右上角的X和Y。
如果您要抓取页面上始终位于同一位置的文本,最简单的方法是使用 Acrobat Pro 的测量工具、Photoshop 或类似工具来测量距页面左下角的距离(以磅为单位),并且使用这些距离来制作像:in_bbox("x0,y0,x1,y1")这样的选择器(有关in_bbox的更多信息,请参见下文)。
如果您正在抓取可能位于页面不同部分的文本,则适用相同的基本技术,但您首先必须找到一个具有一致文本的元素,该元素与您想要的文本的距离一致,然后计算bbox 相对于该元素。有关该方法的示例,请参阅快速入门。
如果这两个都失败了,你最好的办法是使用`pdf.tree.write(filename, pretty_print=True)`转储 xml ,看看你是否能找到任何其他结构、标签或元素来可靠地识别你的部分重新寻找。当您试图找出选择器不匹配的原因时,这也很有帮助……
自定义选择器
pdf.pq 返回的 pyquery 版本支持一些特定于 PDF 的选择器以在页面上按位置查找元素。
:in_bbox(“x0,y0,x1,y1”):仅匹配完全适合给定 bbox 的元素。
:overlaps_bbox(“x0,y0,x1,y1”):匹配与给定 bbox 重叠的任何元素。
如果您需要一个不支持的选择器,您可以编写一个返回布尔值的过滤函数:
>>> def big_elements():
return float(this.get('width',0)) * float(this.get('height',0)) > 40000
>>> pdf.pq('LTPage[page_index="1"] *').filter(big_elements)
[<LTTextBoxHorizontal>, <LTRect>, <LTRect>]
(如果您想出任何特别有用的过滤器,请将它们作为选择器修补到 pdfquery.py 中并提交拉取请求......)
缓存
PDFQuery 接受一个可选的缓存参数,该参数将存储 PDF 解析的结果,因此对同一文件的后续运行会快得多。例如:
from pdfquery.cache import FileCache
pdfquery.PDFQuery("tests/samples/IRS_1040A.pdf", parse_tree_cacher=FileCache("/tmp/"))
批量数据抓取
通常你会想要从 PDF 中获取一堆不同的数据,使用相同的重复过程:(1)使用 pyquery 选择器或 Xpath 查找文档的元素;(2) 解析结果文本;(3) 将其存储在字典中以备后用。
extract方法简化了这个过程。给定关键字和选择器列表:
>>> pdf.extract([
('last_name', ':in_bbox("315,680,395,700")'),
('year', ':contains("Form 1040A (")', lambda match: int(match.text()[-5:-1]))
])
`extract`方法返回一个字典(默认情况下),其中每个关键字都有一个 pyquery 结果集,可以通过提供的格式化函数进行处理。在这个例子中,结果是:
{'last_name': [<LTTextLineHorizontal>], 'year': 2007}
(以('with_formatter', 'text')开头通常很有帮助,因此您会得到类似“Michaels”而不是 [<LTTextLineHorizontal>]的结果。有关更多信息,请参阅下面的特殊关键字。)
搜索目标
默认情况下,extract搜索整个树(或先前由load()加载的文档部分,如果它仅限于特定页面)。如果您想将搜索限制在您之前使用 pdf.pq()获取的树的一部分,请将其作为搜索列表之后的第二个参数传递。
格式化函数
请注意,上面的“年份”示例包含一个可选的第三个参数——一个格式化函数。格式化函数将传递一个 pyquery 匹配结果,因此lambda match: match.text()将返回匹配元素的文本内容。
过滤功能
选择器可以是返回布尔值的过滤函数,而不是字符串:
>>> pdf.extract([('big', big_elements)])
{'big': [<LTPage>, <LTTextBoxHorizontal>, <LTRect>, <LTRect>, <LTPage>, <LTTextBoxHorizontal>, <LTRect>]}
(有关如何定义big_elements 之类的函数,请参阅上面的自定义选择器。)
特殊关键字
extract还会在搜索列表中查找两个特殊关键字,这些关键字为后面列出的搜索设置默认值。请注意,您可以多次包含相同的特殊关键字来更改设置,如快速启动部分所示。关键字是:
with_parent
with_parent关键字将以下搜索限制为父搜索的子搜索。例如:
>>> pdf.extract([ ('with_parent','LTPage[page_index="1"]'), ('last_name', ':in_bbox("315,680,395,700")') # only matches elements on page 1 ])
with_formatter
with_formatter关键字设置了一个默认的格式化函数,除非提供了一个特定的函数,否则它将被调用。例如:
('with_formatter', lambda match: int(match.text()))
将尝试将以下所有搜索结果转换为整数。如果您提供字符串而不是函数,它将被解释为在 pyquery 搜索结果上调用的方法名称。例如,以下两行是等价的:
('with_formatter', lambda match: match.text())
('with_formatter', 'text')
如果要停止过滤结果,可以使用:
('with_formatter', None)
对象参考
公共方法
PDFQuery( file,
merge_tags=('LTChar', 'LTAnon'),
round_floats=True,
round_digits=3,
input_text_formatter=None,
normalize_spaces=True,
resort=True,
parse_tree_cacher=None,
laparams={'all_texts':True, 'detect_vertical':True})
初始化函数。通常你只需要传入文件(文件对象或路径)。其余参数控制元素树的预处理:
merge_tags:这些元素的连续运行将被合并在一起,随后元素的文本附加到第一个元素。这对于减小树的大小很有用,但如果您想选择单个字符而不考虑其容器,则将其关闭可能会有所帮助。
round_floats 和 round_digits:如果 round_floats 为 True,数字将四舍五入到 round_digits 位。这几乎总是好的。
input_text_formatter:一个函数,它接受一个字符串并返回一个修改后的字符串,以应用于元素的文本内容。
normalize_spaces:如果为真(并且未设置 input_text_formatter),则设置 input_text_formatter 以将 s+ 替换为单个空格。
手段:如果为True,元素将被排序,使得任何完全在另一个元素的边界框内的元素都成为该元素的子元素。
parse_tree_cacher:一个知道如何保存和加载从给定 PDF 解析给定页面范围的结果的对象。传入 FileCache('/tmp/') 以将缓存保存到文件系统。
laparams:用于初始化 pdfminer.converter.PDFPageAggregator的pdfminer.layout.LAParams对象的参数。可以是dict、LParams()或None。
extract( searches,
tree=None,
as_dict=True)
请参阅“批量数据抓取”。
搜索:要运行的搜索列表,每个搜索由关键字、选择器和可选的格式化函数组成。
树:运行搜索的 pyquery 树。默认情况下,以 pdf.load() 加载的整个树为目标
as_dict:如果更改为 False,将返回一个列表而不是 dict 以保留结果的顺序。
load(*page_numbers)
初始化 pdf.tree 和 pdf.pq 对象。这将由 pdf.extract() 隐式调用,但仅使用所需的页码显式调用它会更有效。页码可以是整数和列表的任意组合,例如pdf.load(0,2,3,[4,5,6],range(10,15))。
如果由于某种原因您想在不加载任何页面的情况下进行初始化(就像您只对文档信息感兴趣),您可以调用pdf.load(None ) 。
公共但不太有用的方法
这些主要在内部使用,但有时可能会有所帮助……
get_layout(page)
给定页码(零索引)或 pdfminer PDFPage 对象,返回该页面的 LTPage 布局对象。
get_layouts()
返回所有布局的列表(相当于为每个页面调用 get_layout())。
get_page(page_number)
给定页码,返回适当的 pdfminer PDFPage 对象。
get_pyquery(tree=None, page_numbers=[])
在 pyquery 中包装给定的 lxml 元素树。如果没有提供树,将从给定的页码或所有页码生成一个。
get_tree(*page_numbers)
为给定的页码生成一个 etree。*page_numbers可以与load()中的形式相同。
基础库的文档
PDFMiner (pdf.doc): pdfminer_homepage , pdfminer_documentation。
LXML.etree (pdf.tree):lxml_homepage,教程。
PyQuery (pdf.pq): pyquery_documentation。