iMind Developers Blog

iMind開発者ブログ

PDFMinerでPDFのテキストと座標を取得

概要

PDFを読み込んでテキストを取得する際に、一緒にそのテキストが文書内のどの座標(x座標, y座標)にいるかも取得したい。

バージョン情報

  • Python 3.7.4
  • pdfminer==20191016

サンプルデータ

Libreofficeのcalcを用いて下記画像のようなPDFファイルを用意した。

f:id:imind:20191018021015p:plain

これを読み込んでテキストと座標を取得する。

単純なテキストの読込み

まずはシンプルな例として座標はなしでテキストのみを取得する。

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を用意します。

f:id:imind:20191018025003p:plain

この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, 例外情報を追記