如何用borb在Python中提取和处理PDF发票

1,418 阅读6分钟

简介

该 ***可移植文档格式(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 invoice 5

用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 匹配,使这一过程更加稳健,更适合未来的发展。