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
に興味があるのかを伝えてみましょう。
例えば、配送情報を抽出してみましょう(ただし、このコードを修正して、任意の領域を取得することもできます)。
borbが
Rectangleをフィルタリングできるようにするために、
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に対して正規表現をマッチングさせました。