【PyMuPDF】PDF内の表以外を抽出する

`PyMuPDF`というPDF内の表抽出ができるライブラリがありますが、削除する直接的なメゾッドがなさそうだったので、何か方法がないか調べてみました。

## 手順
本記事では以下の手順で実装しています。
1. PDFを読み込み
2. 表の座標を抽出
3. 座標を使って表を塗りつぶし保存
4. 再度PDFを読み込み

## 注意事項
そもそもですが、私の経験上`PyMuPDF`はすべての表を認識できる訳ではありません。
例えば縦線が入っていない表は表と認識されません。
また逆に画像中のテキストなど、表ではないのに表と認識されてしまうケースもあります。

今回は`PyMuPDF`が正しく表と認識できるPDFを使用することを前提として記載しています。

## 実行環境
- Google Colaboratory
- Python: 3.10.12
- PyMuPDF: 1.24.11

## 参考にしたドキュメント・サイト
- `pymupdf`の基本的な使い方
https://pymupdf.readthedocs.io/ja/latest/the-basics.html#extracting-tables-from-a-page
- `bbox`の使い方
https://medium.com/@pymupdf/table-recognition-and-extraction-with-pymupdf-54e54b40b760
- `add_redact_annot`と`apply_redactions`の使い方
https://pymupdf.readthedocs.io/en/latest/page.html#redactions


## 実装
### ①PDFを読み込み
今回はこちらの論文を使用させていただきました。
「電車混雑予測 ～混雑の可視化が社会にもたらすインパクト～」
https://data.navitime.co.jp/pdf/monograph_20170610_2.pdf

colaboにはデフォルトで`pymupdf`が入っていないので`pip`します。
```python: ライブラリをインストール
!pip install pymupdf
```
早速ファイルを読み込み、正しく読み込めているか確認します。
今回はp. 12の表7を使用します。

```python: PDFを読み込み
import pymupdf

# PDFファイルを読み込み
pdf_path = "/content/sample.pdf"
doc = pymupdf.open(pdf_path)
page = doc[11] #12ページ目を指定
page.get_text()
```

【出力結果】
※全体が見えるように各文で改行しています
 ```
 \n12 \n表-7  運賃収入減少上位路線 \n線名 \n収支（円/日） 割合（％） \nＪＲ山手線 \n-262866  \n-0.098  \nＪＲ東海道本線 \n-129302  \n-0.068  \nＪＲ京浜東北線 \n-94601  \n-0.045  \nＪＲ宇都宮線〔東北本線〕 \nＪＲ上野東京ライン \n-64110  \n-0.046  \n京急本線 \n-58849  \n-0.065  \n東武東上線 \n-51553  \n-0.055  \n京急空港線 \n-48776  \n-0.994  \n小田急小田原線 \n-44747  \n-0.026  \n都営浅草線 \n-34341  \n-0.049  \n東武野田線 \n-30825  \n-0.986  \n \n表6，表7 は利用人員の増減を金額換算した表である．
 \n収支の変化が大きい上位20 路線を掲載している．
 黒字\nは増額，赤字は減額を表す．鉄道運賃は同一事業者内で\nは路線ごとの運賃というものはないが，距離に比例して\n運賃を按分することに仮想的に路線ごとの運賃を算出し\nた...
 ```

 表7の情報も抽出されていることが確認できました。

 ### ②表の座標を抽出
 まず表が正しく抽出できているか確認します。
 ```python: 表を抽出
 tabs = page.find_tables()
print(f"{len(tabs.tables)} found on {page}") 
```

【出力結果】
 ```
 1 found on page 11
  ```
p. 12には1つしか表がないので、正しく認識できていると考えます。

次に座標を抽出します。
```python: 表の座標を抽出
tab = tabs[0]
rect = tab.bbox
rect
```

【出力結果】
```
(51.21999979019165, 69.77996826171875, 278.46998087565106, 240.32000732421875)
```

余談ですが、今回はp. 12のテーブルは1つですが、`tabs`と`tab`は異なるクラスに属するので、上記を`tabs.bbox`で取得しようとするとエラーが返ってきます。
いくつ目のテーブルか指定してから座標を抽出する必要がありそうです。
```python: tabs.bboxで座標を取得しようとする
tabs.bbox
```

【出力結果】
```
AttributeError: 'TableFinder' object has no attribute 'bbox'
```

```python: オブジェクトの型を確認
print(type(tabs))
print(type(tab))
```

【出力結果】
```
<class 'pymupdf.table.TableFinder'>
<class 'pymupdf.table.Table'>
```

### ③座標を使って表を塗りつぶし保存

```python: 表を塗りつぶし+保存
# 赤字で塗りつぶし
page.add_redact_annot(rect, fill=(1, 0, 0)) # 赤字を指定
page.apply_redactions() 

# 塗りつぶししたPDFを保存
output_path = "masked_pdf.pdf"
doc.save(output_path)
doc.close()
```

### ④再度PDFを読み込み
保存したPDFを読み込んで表の内容が削除されているか確認します。
```python: 中身の確認
doc_new = pymupdf.open(output_path)
page_new = doc_new[11]
page_new.get_text()
```

【出力結果】
※全体が見えるように各文で改行しています
```
 \n12 \n表-7  運賃収入減少上位路線 \n \n表6，表7 は利用人員の増減を金額換算した表である．\n収支の変化が大きい上位20 路線を掲載している．
 黒字\nは増額，赤字は減額を表す．鉄道運賃は同一事業者内で\nは路線ごとの運賃というものはないが，距離に比例して\n運賃を按分することに仮想的に路線ごとの運賃を算出し\nた．
 乗継割引がある会社をまたいだ時の運賃も同様に距\n離按分した． 
 \n結果としては概ね路線別増減と一致している．
 １日あ\nたり200 人増減，1 人あたりの運賃が200 円とすれば運\n賃収入は1日4万円前後の変化があることになり，結果\nは妥当だと考えられる．年額換算すると平日が年間250\n日とすれば1 路線1000 万円前後の変化がある計算にな\nる...
```

テーブル処理前の内容と比較してみると、「線名〜〜」意向が削除されており、表7の後の内容からが抽出されています。
念の為、`masked_pdf.pdf`をローカルにダウンロードし、PDFを見に行ってみます。

![スクリーンショット 2024-10-05 15.16.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3891949/b5b3859f-f67e-a279-b649-63ce8dd0364e.png)

上記のように赤く塗りつぶされていることがわかります。


## langchainの`PyMuPDFLoader`の更新について（2025/4/23追記）
`langchain_community`がアップグレードされ、（恐らく`langchain_community>=0.3.15`では、私調べ）`PyMuPDFLoader`に新しく`extract_tables`という引数が指定できるようになりました。
公式ドキュメント: https://python.langchain.com/api_reference/community/document_loaders/langchain_community.document_loaders.pdf.PyMuPDFLoader.html#pymupdfloader
`extract_tables=None`にすればPDFのテーブルを除いてテキストのみ抽出できるのでは？？と思ったのですが、、、、
→結論から言うとダメでした
本記事の①PDFを読み込みと同じデータを使って確認してみます。
```python: PDFを読み込み
from langchain_community.document_loaders import PyMuPDFLoader

loader = PyMuPDFLoader(
    file_path = "/content/sample.pdf",
    extract_tables=None
)

doc = loader.load()
page = doc[11]
page.page_content
```

【出力結果】
※全体が見えるように各文で改行しています
 ```
12 \n表-7  運賃収入減少上位路線 \n線名 \n収支（円/日） 割合（％） \nＪＲ山手線 \n-262866  \n-0.098  \nＪＲ東海道本線 \n-129302  \n-0.068  \nＪＲ京浜東北線 \n-94601  \n-0.045  \nＪＲ宇都宮線〔東北本線〕 \nＪＲ上野東京ライン \n-64110  \n-0.046  \n京急本線 \n-58849  \n-0.065  \n東武東上線 \n-51553  \n-0.055  \n京急空港線 \n-48776  \n-0.994  \n小田急小田原線 \n-44747  \n-0.026  \n都営浅草線 \n-34341  \n-0.049  \n東武野田線 \n-30825  \n-0.986  \n \n表6，表7 は利用人員の増減を金額換算した表である．
\n収支の変化が大きい上位20 路線を掲載している．
黒字\nは増額，赤字は減額を表す．鉄道運賃は同一事業者内で\nは路線ごとの運賃というものはないが，距離に比例して\n運賃を按分することに仮想的に路線ごとの運賃を算出し\nた...
 ```
1行目の「\n線名 \n収支（円/日） 割合（％）〜」以降がテーブルの情報なのでそこが削除されるのかなと思ったのですが、表示されてしまいました。
`extract_tables='csv'`にするとテキストの後ろにcsvでテーブルの情報が表示されたので、ある特定の形式で抽出するためのパラメーターだと理解しました。

## 終わりに
少し手間のかかる方法ですが、参考になれば幸いです。
もしもっといい方法があればぜひ教えていただきたいです。