概要
PDFを読み込んでテキストを取得する際に、一緒にそのテキストが文書内のどの座標(x座標, y座標)にいるかも取得したい。
バージョン情報
- Python 3.7.4
- pdfminer==20191016
サンプルデータ
Libreofficeのcalcを用いて下記画像のようなPDFファイルを用意した。
これを読み込んでテキストと座標を取得する。
単純なテキストの読込み
まずはシンプルな例として座標はなしでテキストのみを取得する。
converterにTextConverterを使うとテキストが取れるらしい。
PDFMinerのtoolsにいる下記コードを参考にした。
https://github.com/euske/pdfminer/blob/master/tools/pdf2txt.py
import io from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import TextConverter from pdfminer.pdfpage import PDFPage resourceManager = PDFResourceManager() output = io.StringIO() device = TextConverter(resourceManager, output) with open('example.pdf', 'rb') as fp: interpreter = PDFPageInterpreter(resourceManager, device) for page in PDFPage.get_pages(fp): interpreter.process_page(page) text = output.getvalue() device.close() output.close() print(text) #=> Sheet1ページ 1セルA-1セルB-2セルA-3セルC-3セルB-4セルD-4
これでテキストは取得できた。
レイアウトの取得
textは上述のTextConverterで取れたが、レイアウト情報を取る場合はPDFPageAggregatorを使う必要があるらしい。
import io from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import PDFPageAggregator from pdfminer.pdfpage import PDFPage resourceManager = PDFResourceManager() device = PDFPageAggregator(resourceManager) with open('example.pdf', 'rb') as fp: interpreter = PDFPageInterpreter(resourceManager, device) for page in PDFPage.get_pages(fp): interpreter.process_page(page) layout = device.get_result() for lt in layout: print('{}, x0={:.2f}, x1={:.2f}, y0={:.2f}, y1={:.2f}, width={:.2f}, height={:.2f}'.format( lt.get_text(), lt.x0, lt.x1, lt.y0, lt.y1, lt.width, lt.height)) device.close()
これで下記のように1文字ごとの座標が取れる。
S, x0=281.80, x1=288.46, y0=772.39, y1=785.21, width=6.66, height=12.82 h, x0=288.49, x1=294.05, y0=772.39, y1=785.21, width=5.56, height=12.82 e, x0=294.08, x1=299.64, y0=772.39, y1=785.21, width=5.56, height=12.82 e, x0=299.58, x1=305.14, y0=772.39, y1=785.21, width=5.56, height=12.82 t, x0=305.17, x1=307.94, y0=772.39, y1=785.21, width=2.77, height=12.82 1, x0=307.87, x1=313.43, y0=772.39, y1=785.21, width=5.56, height=12.82 ペ, x0=279.50, x1=289.09, y0=59.00, y1=71.08, width=9.59, height=12.08 ー, x0=289.09, x1=298.48, y0=59.00, y1=71.08, width=9.39, height=12.08 ジ, x0=298.48, x1=307.47, y0=59.00, y1=71.08, width=8.99, height=12.08 以下略
for lt in layout のところで取れているobjectはLTCharクラス。1文字ずつ入るクラスなのでテキスト解析にはちょっと向かない。
テキストとその座標の取得
本題となるテキストとその座標の取得コード。
stackoverflowによるとLAParamsを引数に入れると、1文字ずつではなくTextBoxやTextLineとして値が取れるらしい。
import io from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import PDFPageAggregator from pdfminer.pdfpage import PDFPage from pdfminer.layout import LAParams resourceManager = PDFResourceManager() # 引数にLAParamsを追加 device = PDFPageAggregator(resourceManager, laparams=LAParams()) with open('example.pdf', 'rb') as fp: interpreter = PDFPageInterpreter(resourceManager, device) for page in PDFPage.get_pages(fp): interpreter.process_page(page) layout = device.get_result() for lt in layout: print('{}, x0={:.2f}, x1={:.2f}, y0={:.2f}, y1={:.2f}, width={:.2f}, height={:.2f}'.format( lt.get_text().strip(), lt.x0, lt.x1, lt.y0, lt.y1, lt.width, lt.height)) device.close()
実行結果
Sheet1, x0=281.80, x1=313.43, y0=772.39, y1=785.21, width=31.63, height=12.82 セルA-1, x0=57.70, x1=92.45, y0=752.09, y1=765.08, width=34.75, height=12.99 セルA-3, x0=57.70, x1=92.45, y0=726.49, y1=739.48, width=34.75, height=12.99 セルB-2, x0=121.70, x1=156.45, y0=739.29, y1=752.28, width=34.75, height=12.99 セルB-4, x0=121.70, x1=157.70, y0=713.69, y1=726.68, width=36.00, height=12.99 セルC-3, x0=185.60, x1=220.86, y0=726.49, y1=739.48, width=35.26, height=12.99 セルD-4, x0=249.60, x1=284.86, y0=713.69, y1=726.68, width=35.26, height=12.99 ページ 1, x0=279.50, x1=315.76, y0=58.09, y1=71.08, width=36.26, height=12.99
セルごとのテキストが取れた。
画像や罫線等のオブジェクトが入っているpdfについて
罫線と画像の入った下記のようなPDFを用意します。
このPDFに対して先ほどのコードを実行するとエラーになります。罫線に対してget_textしようとしてエラーになっているようです。
AttributeError: 'LTLine' object has no attribute 'get_text'
layout.pyのコードを見るとテキスト周りのクラスはLTTextContainerを継承していたので、型チェックを追加してエラーを防いで見る。
https://github.com/euske/pdfminer/blob/master/pdfminer/layout.py
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import PDFPageAggregator from pdfminer.pdfpage import PDFPage from pdfminer.layout import LAParams, LTTextContainer resourceManager = PDFResourceManager() device = PDFPageAggregator(resourceManager, laparams=LAParams()) with open('example.pdf', 'rb') as fp: interpreter = PDFPageInterpreter(resourceManager, device) for page in PDFPage.get_pages(fp): interpreter.process_page(page) layout = device.get_result() for lt in layout: # LTTextContainerの場合だけ標準出力 if isinstance(lt, LTTextContainer): print('{}, x0={:.2f}, x1={:.2f}, y0={:.2f}, y1={:.2f}, width={:.2f}, height={:.2f}'.format( lt.get_text().strip(), lt.x0, lt.x1, lt.y0, lt.y1, lt.width, lt.height)) device.close()
これでだいたい欲しい情報が取れそう。
2019.10.25 追記
いくつかのファイルで下記の例外に遭遇した。
例外1
TypeError: unsupported operand type(s) for +: 'PDFObjRef' and 'bytes'
例外2
ValueError: Unsupported predictor value: 2
pdfminer3に切り替えたところ、上記の問題は解消した(converterにcodec=utf8を指定しても動くという情報もあったけど、どっちが良いのだろう?)。
インストール
$ pip uninstall pdfminer $ pip install pdfminer3
あとはimportしているところをpdfminer3に書き換える。
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter ↓ from pdfminer3.pdfinterp import PDFResourceManager, PDFPageInterpreter
とりあえずこれで様子見してみる。
改定履歴
Author: Masato Watanabe, Date: 2019-10-18, 記事投稿 Author: Masato Watanabe, Date: 2019-10-25, 例外情報を追記