python で PDF の指定範囲のテキストを抽出

python の PyMuPDF で PDF から取得したテキスト要素の位置情報を使って、指定範囲のテキスト要素のみを抽出します。


# やりたいこと
PDF からテキストを抽出する際、ヘッダ、フッタ、ページ番号などを除外して、本文だけを抽出できるようにします。
ここでは、本文領域は何らかの方法で取得済とします。


# PDF の座標
PDF の座標は左下が基点で x軸は右向き、y軸は上向きです。
一方、PyMuPDF では左上が基点で x軸は右向き、y軸は下向きです。
https://pymupdf.readthedocs.io/ja/latest/app3.html#coordinates


# プログラム
## 本体
「[python で２つの PDF のテキストの差分を検出](https://qiita.com/dakikd/items/7a646f2b4555fc308000)」で PyMuPDF を使用してテキストを抽出しました。

ここでは、このプログラムを流用して、PDF の範囲を指定してテキストを抽出します。

コンストラクタの引数に x_min y_min, x_min, x_max が指定されている場合には、get_text_objs_from_page() で指定範囲内のテキストのみを抽出します。
```python:pdf_range_text_matcher.py
import sys
import re
import fitz

from pdf_text_matcher import PdfTextMatcher


class PdfRangeTextMatcher(PdfTextMatcher):

    def __init__(self, pdf1, pdf2,
                 x_min=None, y_min=None, x_max=None, y_max=None):
        # 範囲指定がある場合
        self.x_min = x_min
        self.y_min = y_min
        self.x_max = x_max
        self.y_max = y_max

        self.range_flag = False
        if x_min != None and y_min != None and x_max != None and y_max != None:
            self.range_flag = True

        super().__init__(pdf1, pdf2)


    def get_text_objs_from_page(self, pdf, page_id):
        page = pdf[page_id]
        blocks = page.get_text('blocks')

        text_objs = []
        for i, obj in enumerate(blocks):
            if self.range_flag:
                # PDF は左下が基点だが PyMuPDF では左上が基点
                left = obj[0]
                top = obj[1]
                right = obj[2]
                bottom = obj[3]
                if not ((self.x_min <= left and right <= self.x_max)
                        and (self.y_min <= top and bottom <= self.y_max)):
                    continue

            text = obj[4]
            text = self.re_strip.sub('', text)
            if text == '': continue

            id = f"id_{page_id}_{i}"
            text_obj = {'id': id, 'page': page_id, 'text': text, 'obj': obj}
            text_objs.append(text_obj)

        return text_objs
```

## 呼び出し元プログラム例
```python:
import sys
import fitz

sys.path.append('lib')
from lib.pdf_range_text_matcher import PdfRangeTextMatcher


pdf1 = fitz.open(sys.argv[1])
pdf2 = fitz.open(sys.argv[1])

# 範囲指定なし
m = PdfRangeTextMatcher(pdf1, pdf2)
text_objs = m.get_text_objs_from_page(pdf1, 0)
print(text_objs)

# 範囲指定あり
range_m = PdfRangeTextMatcher(pdf1, pdf2, x_min=80, y_min=70, x_max=500, y_max=720)
range_text_objs = range_m.get_text_objs_from_page(pdf1, 0)
print(range_text_objs)
```

## テキスト抽出元の PDF のイメージ
Word でヘッダに「ヘッダ」、フッタに「フッタ」、右下にページ番号が入った PDF を作成しました。
```text:
ヘッダ

あああああ
いいいいい
ううううう
えええええ
おおおおお
かかかかか
ききききき
くくくくく
けけけけけ
こここここ
さささささ
                    1
フッタ
```

## 実行結果
※見やすさのため加工しています。

範囲指定がない場合には、ヘッダ、フッタ、ページ番号を含むテキストが抽出されます。
ヘッダ、フッタ、ページ番号はページ内の位置にかかわらず、先頭の方に出力されました。
```text:範囲指定なし
[{'id': 'id_0_0', 'page': 0, 'text': 'ヘッダ', 'obj': (85.08000183105469, 44.57242965698242, 119.39065551757812, 58.153072357177734, ...)},
 {'id': 'id_0_2', 'page': 0, 'text': '1', 'obj': (504.5999755859375, 759.1724243164062, 512.9320678710938, 772.7530517578125, ...)},
 {'id': 'id_0_3', 'page': 0, 'text': 'フッタ', 'obj': (85.08000183105469, 776.6923828125, 119.39065551757812, 790.2730102539062, ...)},
 {'id': 'id_0_4', 'page': 0, 'text': 'あああああ', 'obj': (85.08000183105469, 101.45242309570312, 140.4058074951172, 115.03306579589844, ...)},
 ...,
 {'id': 'id_0_37', 'page': 0, 'text': 'ししししし', 'obj': (85.08000183105469, 695.4524536132812, 140.4058074951172, 709.0330810546875, ...)}]
```

範囲指定がある場合にはヘッダ、フッタ、ページ番号以外のテキストが抽出されています。
```text:範囲指定あり
[{'id': 'id_0_4', 'page': 0, 'text': 'あああああ', 'obj': (85.08000183105469, 101.45242309570312, 140.4058074951172, 115.03306579589844, ...)},
 {'id': 'id_0_7', 'page': 0, 'text': 'いいいいい', 'obj': (85.08000183105469, 155.45242309570312, 140.4058074951172, 169.03306579589844, ...)},
 ...,
 {'id': 'id_0_37', 'page': 0, 'text': 'ししししし', 'obj': (85.08000183105469, 695.4524536132812, 140.4058074951172, 709.0330810546875, ...)}]
```