Pythonでborbを使ったPDF請求書の抽出と処理

Portable Document Format (PDF) は、WYSIWYG (What You See is What You Get) フォーマットではありません。

プラットフォームに依存せず、基盤となるオペレーティングシステムやレンダリングエンジンから独立するように開発されました。

これを実現するために、PDFはプログラミング言語のようなもので操作できるように構築されており、結果を得るためには一連の命令と操作に依存することになります。

実際、PDFはスクリプト言語であるPostScriptをベースにしています。

PostScriptは、デバイスに依存しない最初のページ記述言語でした。

このガイドでは、PDF文書の読み取り、操作、生成に特化したPythonライブラリであるborbを使用します。

borbは、低レベルモデル(正確な座標やレイアウトにアクセスできる)と高レベルモデル(余白や位置などの正確な計算をレイアウトマネージャーに委ねることができる)の両方を提供します。

このガイドでは、Pythonでborbを使ってPDFの請求書を処理する方法について見ていきます。

処理の自動化は機械の基本的な目標の1つです。

もし誰かが、人間向けの請求書と一緒に json などの解析可能なドキュメントを提供しない場合、自分でPDFの内容を解析する必要が出てくるでしょう。

borbのインストール

borbはGitHubのソースからダウンロードするか、pip経由でインストールすることができます。

$ pip install borb


Pythonでborbを使ってPDFの請求書を作成する。

前回のガイドでは、borbを使用して、PDFの請求書を作成しましたが、今回はその処理を行います。

Pythonでborbを使って請求書を作成する方法について、もっと詳しく知りたい方は、こちらをご覧ください。

生成された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では、Documentのパース処理を行うためのEventListener` クラスを登録することで、これを実現することができる。

例えば、 borb が何らかのテキストレンダリング命令に遭遇するたびに、登録されたすべての EventListener オブジェクトに通知され、そのオブジェクトは生成された Event を処理することができます。

borbにはEventListener` の実装が多数用意されています。

  • SimpleTextExtraction : PDF からテキストを抽出する。
  • SimpleImageExtraction : PDF から画像を抽出する。PDF からすべての画像を抽出する
  • RegularExpressionTextExtraction: 正規表現にマッチしたテキストを抽出する。正規表現にマッチし、ページごとにマッチしたものを返す
  • etc.

まずは、全てのテキストを抽出することから始めましょう。

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


もちろん、これは私たちにとってあまり有益なものではありません。

このコードを改良して、 borb にどの Rectangle に興味があるのかを伝えてみましょう。

例えば、配送情報を抽出してみましょう(ただし、このコードを修正して、任意の領域を取得することもできます)。

borbRectangleをフィルタリングできるようにするために、LocationFilterクラスを使用することにします。

このクラスはEventListenerを実装している。

Page のレンダリング時にすべての Event の通知を受け、あらかじめ定義された範囲内で発生したイベントを(子オブジェクトに)渡します。

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 を削除することから始めましょう。

RegisterExpressionTextExtractionは正規表現にマッチして、(とりわけ)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]点)は左下隅にあることも覚えておいてください。

そのため、「ページ」の上部が最も高い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]


そして、抽出したいテキストさえわかっていれば、座標を取得して、ページ上の矩形内のコンテンツを取得することができます。

結論

このガイドでは、Pythonでborbを使用して請求書を処理する方法について見てきました。

まず、すべてのテキストを抽出することから始め、関心のある領域だけを抽出するように処理を改良しました。

最後に、より堅牢で将来性のある処理を行うために、PDFに対して正規表現をマッチングさせました。

タイトルとURLをコピーしました