简介
该 ***可移植文档格式(PDF)***并不是一种 ***所见即所得(WYSIWYG)。***格式。它被开发成与平台无关,与底层操作系统和渲染引擎无关。
为了实现这一点,PDF的构造是通过更像编程语言的东西进行交互,并依靠一系列的指令和操作来实现结果。事实上,PDF是基于一种脚本语言--PostScript,它是第一个独立于设备的页面描述语言。
在本指南中,我们将使用 borb- 一个专门用于阅读、操作和生成 PDF 文档的 Python 库。它提供了一个低级别的模型(如果你选择使用精确的坐标和布局,允许你访问这些)和一个高级别的模型(你可以将边距、位置等的精确计算委托给一个布局管理器)。
在本指南中,我们将看看如何用borb在Python中处理一张PDF发票,通过提取文本,因为PDF是一种可提取的格式--这使得它容易被自动化处理。
自动处理是机器的基本目标之一,如果有人不提供可解析的文档,比如json
,同时提供面向人类的发票--你就必须自己解析PDF内容。
安装borb
borb可以从GitHub上的源代码下载,或者通过pip
。
$ pip install borb
**注意:**如果你在 Windows 上工作,你可能需要安装一个额外的依赖。
$ pip install windows-curses
用 borb 在 Python 中创建 PDF 发票
在前面的指南中,我们已经用 borb 生成了一张 PDF 发票,现在我们将对其进行处理。
生成的PDF文档具体看起来是这样的。
用borb处理一个PDF发票
让我们从打开PDF文件开始,并将其加载到Document
- 文件的对象表示。
import typing
from borb.pdf.document import Document
from borb.pdf.pdf import PDF
def main():
d: typing.Optional[Document] = None
with open("output.pdf", "rb") as pdf_in_handle:
d = PDF.loads(pdf_in_handle)
assert d is not None
if __name__ == "__main__":
main()
该代码遵循你可能在json
库中看到的相同模式;一个静态方法,loads()
,它接受一个文件手柄,并输出一个数据结构。
接下来,我们希望能够提取文件的所有文本内容。borb
,允许你将EventListener
类注册到Document
的解析中,从而实现这一目标。
例如,每当borb
遇到某种文本渲染指令,它就会通知所有注册的EventListener
对象,然后这些对象就可以处理发出的Event
。
borb
EventListener
的相当多的实现。
SimpleTextExtraction
: 从PDF中提取文本SimpleImageExtraction
:从PDF中提取所有图像RegularExpressionTextExtraction
:匹配一个正则表达式,并返回每页的匹配结果- 等等。
我们将从提取所有文本开始。
import typing
from borb.pdf.document import Document
from borb.pdf.pdf import PDF
# New import
from borb.toolkit.text.simple_text_extraction import SimpleTextExtraction
def main():
d: typing.Optional[Document] = None
l: SimpleTextExtraction = SimpleTextExtraction()
with open("output.pdf", "rb") as pdf_in_handle:
d = PDF.loads(pdf_in_handle, [l])
assert d is not None
print(l.get_text_for_page(0))
if __name__ == "__main__":
main()
这个代码片段应该按照阅读顺序(从上到下,从左到右)打印发票中的所有文本。
[Street Address] Date 6/5/2021
[City, State, ZIP Code] Invoice # 1741
[Phone] Due Date 6/5/2021
[Email Address]
[Company Website]
BILL TO SHIP TO
[Recipient Name] [Recipient Name]
[Company Name] [Company Name]
[Street Address] [Street Address]
[City, State, ZIP Code] [City, State, ZIP Code]
[Phone] [Phone]
DESCRIPTION QTY UNIT PRICE AMOUNT
Product 1 2 $ 50 $ 100
Product 2 4 $ 60 $ 240
Labor 14 $ 60 $ 840
Subtotal $ 1,180.00
Discounts $ 177.00
Taxes $ 100.30
Total $ 1163.30
当然,这对我们来说不是很有用,因为这需要更多的处理,然后才能做很多事情,虽然这是一个很好的开始,特别是与OCR扫描的PDF文件相比
让我们细化这段代码,告诉
borb
,我们对哪些Rectangle
。
例如,让我们提取运输信息(但你可以修改代码以检索任何感兴趣的领域)。
为了让borb
,以过滤出一个Rectangle
,我们将使用LocationFilter
类。这个类实现了EventListener
。它在渲染Page
时得到所有Events
的通知,并将那些发生在预先定义的边界内的(给它的子类)传递出去。
import typing
from decimal import Decimal
from borb.pdf.document import Document
from borb.pdf.pdf import PDF
from borb.toolkit.text.simple_text_extraction import SimpleTextExtraction
# New import
from borb.toolkit.location.location_filter import LocationFilter
from borb.pdf.canvas.geometry.rectangle import Rectangle
def main():
d: typing.Optional[Document] = None
# Define rectangle of interest
# x, y, width, height
r: Rectangle = Rectangle(Decimal(280),
Decimal(510),
Decimal(200),
Decimal(130))
# Set up EventListener(s)
l0: LocationFilter = LocationFilter(r)
l1: SimpleTextExtraction = SimpleTextExtraction()
l0.add_listener(l1)
with open("output.pdf", "rb") as pdf_in_handle:
d = PDF.loads(pdf_in_handle, [l0])
assert d is not None
print(l1.get_text_for_page(0))
if __name__ == "__main__":
main()
运行这段代码,假设选择了正确的矩形,就会打印出来。
SHIP TO
[Recipient Name]
[Company Name]
[Street Address]
[City, State, ZIP Code]
[Phone]
这段代码并不完全是最灵活或最适合未来的。要找到正确的Rectangle
,需要花些功夫,而且不能保证在发票的布局稍有变化时它也能工作。
我们将不得不建立一些更强大的东西,以获得实际的应用。
我们可以从删除硬编码的Rectangle
开始。RegularExpressionTextExtraction
可以匹配正则表达式,并返回(除其他外)它在Page
上的坐标!使用模式匹配,我们可以自动搜索文档中的元素并检索它们,而不是猜测在哪里画一个矩形。
让我们用这个类来寻找 "SHIP TO "这个词,并根据这些坐标建立一个Rectangle
。
import typing
from borb.pdf.document import Document
from borb.pdf.pdf import PDF
from borb.pdf.canvas.geometry.rectangle import Rectangle
# New imports
from borb.toolkit.text.regular_expression_text_extraction import RegularExpressionTextExtraction, PDFMatch
def main():
d: typing.Optional[Document] = None
# Set up EventListener
l: RegularExpressionTextExtraction = RegularExpressionTextExtraction("SHIP TO")
with open("output.pdf", "rb") as pdf_in_handle:
d = PDF.loads(pdf_in_handle, [l])
assert d is not None
matches: typing.List[PDFMatch] = l.get_matches_for_page(0)
assert len(matches) == 1
r: Rectangle = matches[0].get_bounding_boxes()[0]
print("%f %f %f %f" % (r.get_x(), r.get_y(), r.get_width(), r.get_height()))
if __name__ == "__main__":
main()
在这里,我们围绕该部分建立了一个Rectangle
,并打印了它的坐标。
299.500000 621.000000 48.012000 8.616000
你会注意到,get_bounding_boxes()
,返回typing.List[Rectangle]
。当一个正则表达式在PDF中的多行文本中被匹配时,就是这种情况。
另外,请记住,PDF的原点(
[0, 0]
点)位于左下角。因此,Page
的顶部有最高的 Y 坐标。
现在我们知道在哪里可以找到*"SHIP TO"*,我们可以更新我们先前的代码,将感兴趣的Rectangle
,就在这些字的下面。
import typing
from decimal import Decimal
from borb.pdf.document import Document
from borb.pdf.pdf import PDF
from borb.pdf.canvas.geometry.rectangle import Rectangle
from borb.toolkit.location.location_filter import LocationFilter
from borb.toolkit.text.regular_expression_text_extraction import RegularExpressionTextExtraction, PDFMatch
from borb.toolkit.text.simple_text_extraction import SimpleTextExtraction
def find_ship_to() -> Rectangle:
d: typing.Optional[Document] = None
# Set up EventListener
l: RegularExpressionTextExtraction = RegularExpressionTextExtraction("SHIP TO")
with open("output.pdf", "rb") as pdf_in_handle:
d = PDF.loads(pdf_in_handle, [l])
assert d is not None
matches: typing.List[PDFMatch] = l.get_matches_for_page(0)
assert len(matches) == 1
return matches[0].get_bounding_boxes()[0]
def main():
d: typing.Optional[Document] = None
# Define rectangle of interest
ship_to_rectangle: Rectangle = find_ship_to()
r: Rectangle = Rectangle(ship_to_rectangle.get_x() - Decimal(50),
ship_to_rectangle.get_y() - Decimal(100),
Decimal(200),
Decimal(130))
# Set up EventListener(s)
l0: LocationFilter = LocationFilter(r)
l1: SimpleTextExtraction = SimpleTextExtraction()
l0.add_listener(l1)
with open("output.pdf", "rb") as pdf_in_handle:
d = PDF.loads(pdf_in_handle, [l0])
assert d is not None
print(l1.get_text_for_page(0))
if __name__ == "__main__":
main()
然后这段代码就打印出来了。
SHIP TO
[Recipient Name]
[Company Name]
[Street Address]
[City, State, ZIP Code]
[Phone]
这仍然需要对文档有一定的了解,但并不像之前的方法那样死板--只要你知道你想提取哪段文字--你就可以得到坐标,并在页面上的一个矩形范围内攫取内容。
总结
在本指南中,我们已经看了如何使用borb在Python中处理一张发票。我们从提取所有的文本开始,然后改进我们的过程,只提取一个感兴趣的区域。最后,我们用正则表达式与 PDF 匹配,使这一过程更加稳健,更适合未来的发展。