PythonでPDFを自由自在に操る!pdfminer.six徹底解説 ⛏️

Python

PDFからの情報抽出、もう迷わない!

はじめに:pdfminer.sixとは? 🤔

pdfminer.six は、Pythonで書かれた強力なPDF解析ライブラリです。PDFドキュメントからテキストやレイアウト情報、メタデータなどを抽出することに特化しています。WebスクレイピングでHTMLを解析するように、PDFファイルの中身をプログラムで扱えるようにしてくれるツール、と考えると分かりやすいかもしれません。📄

このライブラリのルーツは、Yusuke Shinyama氏によって開発された pdfminer にあります。オリジナルの pdfminer は長らくメンテナンスが停止していましたが、コミュニティによってフォークされ、Python 3への対応や機能改善が行われたのが pdfminer.six です。現在も活発に開発が続けられており、PDF解析ライブラリの有力な選択肢の一つとなっています。

なぜ pdfminer.six を選ぶのか?
  • 詳細なレイアウト分析: 他のライブラリと比較して、テキストの位置情報(座標)や、テキストブロック、行、文字といった構造を詳細に解析できる点が大きな強みです。これにより、単なるテキスト抽出だけでなく、表形式のデータ抽出など、より高度な処理が可能になります。
  • 多様な出力形式: 抽出した情報をプレーンテキストだけでなく、HTML、XML、タグ付きPDFなど、様々な形式で出力できます。
  • Python 3 完全対応: 最新のPython環境で安心して利用できます。
  • 活発な開発: コミュニティによる継続的な開発が行われており、バグ修正や機能追加が期待できます。
  • コマンドラインツール同梱: 簡単なテキスト抽出なら、Pythonスクリプトを書かずにコマンドラインから実行できます。

このブログでは、pdfminer.six のインストールから基本的な使い方、主要な機能、そして少し高度な活用方法まで、具体的なコード例を交えながら詳しく解説していきます。この記事を読めば、あなたも pdfminer.six を使ってPDFから自由に情報を取り出せるようになるはずです!💪

インストールと基本的な使い方 🚀

pdfminer.six のインストールは、Pythonのパッケージ管理ツールであるpipを使うのが最も簡単です。ターミナル(コマンドプロンプト)を開き、以下のコマンドを実行してください。

pip install pdfminer.six

これで、あなたのPython環境に pdfminer.six がインストールされます。🎉

pdfminer.six には、pdf2txt.py という便利なコマンドラインツールが同梱されています。これを使うと、簡単なテキスト抽出であればPythonスクリプトを書かなくても実行できます。

基本的な使い方は以下の通りです。

pdf2txt.py input.pdf

上記コマンドは、input.pdf ファイルからテキストを抽出し、標準出力(通常はターミナル画面)に表示します。

ファイルに出力したい場合は、-o オプションを使います。

pdf2txt.py -o output.txt input.pdf

特定のページだけを抽出したい場合は、-p オプションでページ番号を指定します(カンマ区切り、ハイフンで範囲指定も可能)。

# 1ページ目と3ページ目を抽出
pdf2txt.py -p 1,3 input.pdf

# 2ページ目から4ページ目までを抽出
pdf2txt.py -p 2-4 input.pdf

HTML形式で出力することも可能です。-t html オプションを使います。

pdf2txt.py -t html -o output.html input.pdf

他にも様々なオプションがあります。-h オプションでヘルプを確認できますので、試してみてください。

pdf2txt.py -h

もちろん、Pythonスクリプト内から pdfminer.six の機能を利用することも可能です。最も基本的なテキスト抽出の例を見てみましょう。

from pdfminer.high_level import extract_text

# PDFファイルパス
pdf_path = 'input.pdf'

try:
    # テキストを抽出
    text = extract_text(pdf_path)
    print(text)
except Exception as e:
    print(f"エラーが発生しました: {e}")

pdfminer.high_level モジュールの extract_text 関数を使うと、非常にシンプルにPDFからテキスト全体を抽出できます。ファイルパスを渡すだけで、抽出されたテキストが文字列として返ってきます。簡単ですね!✨

extract_text 関数には、コマンドラインツールと同様に、ページ指定 (page_numbers)、パスワード (password)、レイアウト解析のパラメータ調整などのオプションも用意されています。

from pdfminer.high_level import extract_text

pdf_path = 'input.pdf'
output_path = 'output.txt'

try:
    # 1ページ目と3ページ目のみ抽出
    text_p1_3 = extract_text(pdf_path, page_numbers=[0, 2]) # ページ番号は0から始まるインデックス
    # print(text_p1_3)

    # パスワード付きPDFの場合 (パスワードが 'secret' だとする)
    # text_pw = extract_text(pdf_path, password='secret')
    # print(text_pw)

    # ファイルに出力する場合
    with open(output_path, "w", encoding='utf-8') as f:
        f.write(extract_text(pdf_path))
    print(f"{output_path} にテキストを出力しました。")

except Exception as e:
    print(f"エラーが発生しました: {e}")

このように、pdfminer.six は基本的なテキスト抽出を手軽に行うための高レベルAPIを提供しています。しかし、このライブラリの真価は、より詳細な情報を取得できる低レベルな機能にあります。次のセクションで詳しく見ていきましょう。

主要な機能詳解 🔬

pdfminer.six は単なるテキスト抽出ツールではありません。PDFの内部構造を深く解析し、様々な情報を取り出すための豊富な機能を提供しています。ここでは、その主要な機能について解説します。

基本的なテキスト抽出は extract_text 関数で簡単に行えましたが、より細かな制御や情報取得を行いたい場合は、低レベルAPIを使用します。これにより、テキストの内容だけでなく、その位置(座標)、フォント情報などを取得できます。

具体的な方法は後述する「レイアウト分析の詳細」で解説しますが、PDFPageInterpreterTextConverter などのクラスを組み合わせることで、文字単位での情報アクセスが可能になります。

pdfminer.six の最も強力な機能の一つが、このレイアウト分析です。PDF内のテキストがどのように配置されているか、段落や表がどのように構成されているかを解析します。

  • テキストブロック (LTTextBox): 段落に相当するような、まとまったテキストの塊を認識します。
  • テキストライン (LTTextLine): テキストの行を認識します。
  • 文字 (LTChar): 個々の文字とその座標、フォント情報を取得できます。
  • 矩形 (LTRect): PDF内の四角形(罫線など)を認識します。表の解析などに役立ちます。
  • 曲線 (LTCurve): PDF内の曲線を認識します。
  • 画像 (LTImage): PDF内の画像を認識し、その位置やサイズを取得できます(画像自体の抽出とは別)。
  • 図形 (LTFigure): テキストや画像などがグループ化された要素を認識します。

これらのレイアウト要素を解析することで、「特定の範囲にあるテキストだけを抽出する」「表形式のデータを列ごとに整理する」といった高度な処理が可能になります。レイアウト分析の詳細は、次のセクションでさらに深掘りします。

PDFに含まれる画像を抽出することも可能です。ただし、pdfminer.six は画像形式の変換は行いません。PDF内部で保存されている形式(多くはJPEG, JBIG2, CCITT Fax Decode など)のまま、バイナリデータとして取り出します。

画像抽出には、通常、低レベルAPIを使用し、ページ内の LTImage オブジェクトを検出して、そのストリームデータをファイルに書き出す、という手順を踏みます。簡単なスクリプト例は後述の「高度な使い方」で触れますが、複雑な画像形式やマスキングが施されている場合、別途画像処理ライブラリ(Pillowなど)が必要になることもあります。

注意: 全ての種類の画像を完璧に抽出できるわけではありません。特に、PDFの仕様は複雑なため、一部の画像形式や特殊な埋め込み方をしている画像は、うまく抽出できない可能性があります。

PDFに埋め込まれた目次(アウトライン、ブックマークとも呼ばれます)を取得する機能も提供されています。pdfminer.pdfdocument.PDFDocument クラスと pdfminer.pdfparser.PDFParser を使うことで、目次の階層構造やリンク先のページ番号などを取得できます。

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdftypes import resolve1 # resolve1 をインポート

# PDFファイルを開く
fp = open('input.pdf', 'rb')
parser = PDFParser(fp)
document = PDFDocument(parser)

# 目次を取得 (存在しない場合は例外が発生する可能性あり)
try:
    outlines = document.get_outlines()
    for (level, title, dest, a, se) in outlines:
        print(f"Level: {level}, Title: {title}")
        # dest や他の情報を使ってさらに詳細な処理が可能
except Exception as e:
    print(f"目次の取得中にエラーが発生しました: {e}")

fp.close()

取得した目次情報を使えば、特定のセクションへジャンプしたり、ドキュメントの構造を把握したりするのに役立ちます。

さらに深くPDFの内部構造にアクセスしたい場合、pdfminer.six はPDFのオブジェクト(辞書、配列、ストリームなど)を直接操作するための機能も提供しています。これは非常に強力ですが、PDFの内部仕様に関する知識が必要となります。

PDFParser, PDFDocument, PDFPage などのクラスを通じて、ページの辞書情報、フォント情報、カラースペース情報などにアクセスできます。特定の注釈(アノテーション)を抽出したり、フォームフィールドのデータを読み取ったりする場合などに利用できます。ただし、これらの操作は複雑になりがちで、エラーハンドリングも重要になります。

抽出・解析した結果は、様々な形式で出力できます。

  • プレーンテキスト (.txt): 最も基本的な形式。
  • HTML (.html): テキストのフォントサイズや位置情報を元に、元のPDFに近いレイアウトを再現しようと試みます。完全な再現は難しいですが、視覚的な構造を保持したい場合に便利です。
  • XML (.xml): テキストやレイアウト要素(座標、フォント情報など)を構造化されたXML形式で出力します。プログラムでの後処理に適しています。
  • タグ付きPDF (Tagged PDF): 解析結果を元に、アクセシビリティ情報(タグ構造)を付与したPDFを生成する機能も実験的に含まれています(高度な機能)。

これらの出力形式は、pdf2txt.py コマンドラインツールの -t オプションや、Pythonスクリプト内で適切な Converter クラス(例: TextConverter, HTMLConverter, XMLConverter)を選択することで利用できます。

レイアウト分析の詳細:座標と構造を掴む 🗺️

pdfminer.six の真骨頂とも言えるレイアウト分析機能について、さらに詳しく見ていきましょう。この機能を使いこなすことで、単なるテキスト抽出を超えた、PDF情報の高度な活用が可能になります。

レイアウト分析の挙動は pdfminer.layout.LAParams クラスによって制御されます。LAParams は “Layout Analysis Parameters” の略で、テキストや図形をどのようにグループ化し、解釈するかのルールを定義します。

主なパラメータには以下のようなものがあります。

パラメータ名 説明 デフォルト値 (例)
line_overlap 文字が垂直方向にどれだけ重なっていたら同じ行にあるとみなすかの閾値 (文字の高さに対する割合)。 0.5
char_margin 文字が水平方向にどれだけ離れていたら同じ単語/行にあるとみなすかの閾値 (文字の幅に対する割合)。 2.0
line_margin 行が垂直方向にどれだけ離れていたら同じテキストブロックにあるとみなすかの閾値 (文字の高さに対する割合)。 0.5
word_margin 単語が水平方向にどれだけ離れていたら同じ行にあるとみなすかの閾値 (文字の幅に対する割合)。デフォルトでは char_margin と同じ値が使われることが多い。 0.1
boxes_flow テキストブロックの読み順を推測する際のアルゴリズムを制御。水平方向か垂直方向か、どの程度のずれを許容するかなどを設定 (-1.0: 無効, 0.5: デフォルト)。 0.5
detect_vertical 縦書きテキストの検出を有効にするか (True/False)。 False
all_texts 図形オブジェクト (LTFigure) 内に含まれるテキストも抽出対象にするか (True/False)。 False

これらのパラメータは、解析対象のPDFのレイアウト特性(文字間隔、行間隔、段組など)に合わせて調整することで、解析精度を向上させることができます。例えば、文字が密なPDFでは char_margin を小さく、行間が広いPDFでは line_margin を大きくするといった調整が考えられます。

extract_text 関数や、後述する低レベルAPIを使う際に、この LAParams オブジェクトを引数として渡すことで、分析挙動をカスタマイズできます。

from pdfminer.high_level import extract_text
from pdfminer.layout import LAParams

pdf_path = 'input.pdf'

# カスタムパラメータを設定
# 例: 文字間のマージンを少し広げ、縦書き検出を有効にする
custom_laparams = LAParams(char_margin=3.0, detect_vertical=True)

try:
    text = extract_text(pdf_path, laparams=custom_laparams)
    print(text)
except Exception as e:
    print(f"エラーが発生しました: {e}")

pdfminer.six は、PDFのページコンテンツを解析し、以下のような階層構造を持つレイアウトオブジェクトを生成します。

ページ (LTPage)
  • テキストボックス (LTTextBox / LTTextBoxVertical): 段落のようなテキストのまとまり。
    • テキストライン (LTTextLine / LTTextLineVertical): テキストの行。
      • 文字 (LTChar / LTAnno): 個々の文字または注釈(スペースなど)。座標、フォント情報、テキスト内容を持つ。
  • 矩形 (LTRect): 罫線などの四角形。座標、線幅、色などの情報を持つ。
  • 曲線 (LTCurve): 曲線。座標、線幅、色などの情報を持つ。
  • 画像 (LTImage): 画像。座標、サイズ、画像名(もしあれば)などの情報を持つ。
  • 図形 (LTFigure): 他のレイアウト要素(テキスト、画像など)をグループ化したもの。再帰的に内部要素を持つ。

これらのオブジェクトはすべて、自身のバウンディングボックス (Bounding Box)、つまり要素が存在する領域を表す矩形の座標 (x0, y0, x1, y1) を bbox 属性として持っています。座標系の原点 (0, 0) はページの左下隅です。

この階層構造と座標情報を利用することで、様々な応用が可能になります。低レベルAPIを使って、これらのオブジェクトにアクセスする方法を見てみましょう。ここでは、特定のテキストボックスの内容とその座標を取得する例を示します。

from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer, LTChar, LAParams

pdf_path = 'input.pdf'

# LAParams で必要に応じてレイアウト分析を調整
laparams = LAParams()

for page_layout in extract_pages(pdf_path, laparams=laparams):
    print(f"--- Page {page_layout.pageid} ---")
    for element in page_layout:
        # LTTextContainer は LTTextBox や LTTextLine の基底クラス
        if isinstance(element, LTTextContainer):
            # 要素のテキスト内容を取得
            text = element.get_text().strip()
            if text: # 空の要素は無視
                # 要素のバウンディングボックス (座標) を取得
                # bbox は (x0, y0, x1, y1) のタプル
                # (x0, y0) が左下、(x1, y1) が右上の座標
                x0, y0, x1, y1 = element.bbox
                print(f"座標: ({x0:.2f}, {y0:.2f}) - ({x1:.2f}, {y1:.2f})")
                print(f"テキスト:\n{text}\n")

        # 文字単位で情報を取りたい場合 (より詳細)
        # if isinstance(element, LTTextBox):
        #     for line in element:
        #         if isinstance(line, LTTextLine):
        #             line_text = ""
        #             for character in line:
        #                 if isinstance(character, LTChar):
        #                     char_text = character.get_text()
        #                     char_x0, char_y0, char_x1, char_y1 = character.bbox
        #                     # print(f"  文字 '{char_text}' at ({char_x0:.2f}, {char_y0:.2f})")
        #                     line_text += char_text
        #                 # LTAnno はスペースなどの注釈文字
        #                 elif isinstance(character, LTAnno):
        #                     line_text += character.get_text()
        #             print(f"  行テキスト: {line_text.strip()}")
        #             print(f"  行座標: ({line.x0:.2f}, {line.y0:.2f}) - ({line.x1:.2f}, {line.y1:.2f})")

このコードは、extract_pages 関数を使ってページごとにレイアウトオブジェクト (LTPage) を取得し、その中の要素をイテレートします。isinstance を使って要素の型をチェックし、LTTextContainer (テキストボックスやテキストライン) であれば、そのテキスト内容と座標 (bbox) を出力しています。コメントアウト部分のように、さらに深く文字 (LTChar) レベルまで掘り下げることも可能です。

座標情報が手に入れば、以下のような応用が考えられます。

  • 特定の座標範囲(ヘッダー、フッター、特定の列など)に含まれるテキストだけを抽出する。
  • テキストのY座標を比較して、表の行を特定する。
  • テキストのX座標を比較して、表の列を特定する。
  • LTRect オブジェクト(罫線)の座標と組み合わせて、表のセル構造を正確に認識する。
  • テキストのフォントサイズや太字などのスタイル情報(LTChar オブジェクトから取得可能)を使って、見出しや強調箇所を特定する。

PDFからの表抽出は、その多様なレイアウトのため、一般的に難しいタスクとされています。しかし、pdfminer.six の詳細なレイアウト分析機能は、この課題に取り組む上で強力な武器となります。

単純な表であれば、テキスト要素のY座標でグルーピングして行を特定し、X座標でソートして列を特定する、といったアプローチが考えられます。罫線 (LTRect) が存在する場合は、その座標を行や列の区切りとして利用することで、より頑健な抽出が可能になります。

ただし、セルが結合されていたり、複雑なレイアウトを持つ表の場合、単純な座標比較だけでは不十分なことも多いです。そのような場合は、テキストブロックや罫線の関係性をより深く分析する、独自のアルゴリズムを実装する必要があります。これは高度なトピックになりますが、pdfminer.six が提供する豊富な情報が、その基盤となるでしょう。💡

レイアウト分析は pdfminer.six の強力な機能ですが、完璧ではありません。特に、スキャンされた画像ベースのPDFや、非常に複雑なレイアウトを持つPDFでは、期待通りの解析結果が得られないこともあります。LAParams の調整や、複数のアプローチを試すことが重要になります。

高度な使い方とTips ✨

基本的な使い方とレイアウト分析をマスターしたら、次は pdfminer.six のさらに高度な機能や、使う上での注意点、Tipsを見ていきましょう。

閲覧にパスワードが必要なPDFファイルも、パスワードが分かっていれば処理できます。extract_textextract_pages などの高レベル関数では、password 引数にパスワード文字列を指定します。

from pdfminer.high_level import extract_text

pdf_path = 'protected.pdf'
password = 'your_password' # 正しいパスワードに置き換えてください

try:
    text = extract_text(pdf_path, password=password)
    print("パスワード付きPDFのテキスト抽出に成功しました。")
    # print(text)
except Exception as e:
    # 不正なパスワードなどの場合、pdfminer.pdfdocument.PDFPasswordIncorrect が発生することがある
    print(f"エラーが発生しました: {e}")

低レベルAPIを使用する場合、PDFDocument オブジェクトを初期化する際にパスワードを指定します。

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument, PDFPasswordIncorrect
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
import io

pdf_path = 'protected.pdf'
password = b'your_password' # パスワードはバイト列で渡すことが多い

laparams = LAParams()
resource_manager = PDFResourceManager()
# 出力先を io.StringIO でメモリ上に確保
fake_file_handle = io.StringIO()
converter = TextConverter(resource_manager, fake_file_handle, laparams=laparams)
page_interpreter = PDFPageInterpreter(resource_manager, converter)

try:
    with open(pdf_path, 'rb') as fp:
        parser = PDFParser(fp)
        # パスワードを指定してドキュメントオブジェクトを作成
        document = PDFDocument(parser, password=password)

        if not document.is_extractable:
             raise PDFTextExtractionNotAllowed("Text extraction is not allowed")

        for page in PDFPage.create_pages(document):
            page_interpreter.process_page(page)

        text = fake_file_handle.getvalue()
        print("パスワード付きPDFのテキスト抽出に成功しました (低レベルAPI)。")
        # print(text)

except PDFPasswordIncorrect:
    print("エラー: パスワードが間違っています。")
except Exception as e:
    print(f"エラーが発生しました: {e}")
finally:
    # リソースを解放
    converter.close()
    fake_file_handle.close()

重要: PDFにはオーナーパスワードとユーザーパスワードの2種類があります。オーナーパスワードは印刷や編集などの権限を制限し、ユーザーパスワードは閲覧自体を制限します。pdfminer.six でテキスト抽出を行うには、通常、ユーザーパスワード(閲覧パスワード)が必要です。また、PDFのセキュリティ設定でテキスト抽出が明示的に禁止されている場合、正しいパスワードを入力しても抽出できないことがあります (is_extractable 属性で確認可能)。

大きなPDFファイルの一部ページだけを処理したい場合、ページ番号を指定して効率化できます。extract_textextract_pages では、page_numbers 引数に処理したいページのインデックス(0から始まる)のリストを渡します。

from pdfminer.high_level import extract_text, extract_pages

pdf_path = 'large_document.pdf'

# 最初のページ (インデックス 0) と 3番目のページ (インデックス 2) のみを抽出
target_pages = [0, 2]

try:
    # extract_text の場合
    text_subset = extract_text(pdf_path, page_numbers=target_pages)
    print(f"--- ページ {', '.join(map(str, [p+1 for p in target_pages]))} のテキスト (extract_text) ---")
    # print(text_subset)

    print("\n" + "="*20 + "\n")

    # extract_pages の場合
    print(f"--- ページ {', '.join(map(str, [p+1 for p in target_pages]))} のレイアウト要素 (extract_pages) ---")
    for page_layout in extract_pages(pdf_path, page_numbers=target_pages):
        print(f"Processing Page {page_layout.pageid}...")
        # ここで page_layout に対する処理を行う
        # 例:
        # for element in page_layout:
        #     if isinstance(element, LTTextContainer):
        #         print(element.get_text().strip())

except Exception as e:
    print(f"エラーが発生しました: {e}")

低レベルAPIを使う場合は、PDFPage.create_pages(document) の代わりに PDFPage.get_pages(document, pagenos=set(target_pages)) を使用します。

これまで紹介してきた TextConverter, HTMLConverter, XMLConverter は、ページ全体の解析が終わった後に、指定された形式で結果を出力するデバイスでした。

一方で、pdfminer.converter.PDFPageAggregator は、ページを解析しながらレイアウトオブジェクト (LTPage, LTTextBox, LTChar など) を構築し、それらをプログラムで直接扱えるようにするデバイスです。これは「レイアウト分析の詳細」セクションで extract_pages 関数の内部で暗黙的に使われていたものです。

PDFPageAggregator を明示的に使うことで、より細かい制御が可能になります。

from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator # Aggregator を使う
from pdfminer.layout import LAParams, LTTextBox, LTTextLine, LTChar

pdf_path = 'input.pdf'

laparams = LAParams()
resource_manager = PDFResourceManager()
# PDFPageAggregator をデバイスとして使用
device = PDFPageAggregator(resource_manager, laparams=laparams)
interpreter = PDFPageInterpreter(resource_manager, device)

try:
    with open(pdf_path, 'rb') as fp:
        parser = PDFParser(fp)
        document = PDFDocument(parser)

        for page in PDFPage.create_pages(document):
            interpreter.process_page(page)
            # ページ全体のレイアウトオブジェクトを取得
            layout = device.get_result()
            print(f"--- Page {layout.pageid} ---")
            # layout オブジェクト (LTPage) を直接操作できる
            for element in layout:
                if isinstance(element, LTTextBox):
                    print(f"TextBox BBox: {element.bbox}")
                    print(element.get_text())

except Exception as e:
    print(f"エラーが発生しました: {e}")
finally:
    # Aggregator は close メソッドを持たないことが多いが、
    # 他の Converter と同様に finally での処理が推奨される場合がある
    # (特にカスタムデバイスの場合)
    pass # device.close() は不要

この方法は、extract_pages を使うのと実質的に同じ結果になりますが、独自の解析ロジックやカスタムデバイスを組み込みたい場合に、より柔軟な対応が可能になります。

前述の通り、pdfminer.six は画像データをそのままの形式で抽出します。以下は、PDFPageAggregator を使ってページ内の LTImage オブジェクトを見つけ、そのストリームデータをファイルに保存する簡単な例です。

import os
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LAParams, LTImage # LTImage をインポート

pdf_path = 'input_with_images.pdf'
output_dir = 'extracted_images'
os.makedirs(output_dir, exist_ok=True)

laparams = LAParams()
resource_manager = PDFResourceManager()
device = PDFPageAggregator(resource_manager, laparams=laparams)
interpreter = PDFPageInterpreter(resource_manager, device)

image_count = 0

try:
    with open(pdf_path, 'rb') as fp:
        parser = PDFParser(fp)
        document = PDFDocument(parser)

        for i, page in enumerate(PDFPage.create_pages(document)):
            interpreter.process_page(page)
            layout = device.get_result()

            for element in layout:
                if isinstance(element, LTImage):
                    image_count += 1
                    image_name = element.name # 画像の内部名
                    stream = element.stream # 画像データのストリームオブジェクト

                    if stream:
                        # ストリームからデータを読み込む
                        image_data = stream.get_data()
                        # 推奨されるファイル拡張子を取得 (もしあれば)
                        file_ext = stream.get_imagemeta().get('Filter', 'bin')
                        if isinstance(file_ext, list): # フィルタが複数適用されている場合あり
                            file_ext = file_ext[0].replace('/','') # 例: /FlateDecode -> FlateDecode
                        else:
                            file_ext = str(file_ext).replace('/','')

                        # ファイル名を生成 (重複回避のためページ番号と連番を追加)
                        output_filename = f"page_{i+1}_img_{image_count}_{image_name}.{file_ext.lower()}"
                        output_path = os.path.join(output_dir, output_filename)

                        try:
                            with open(output_path, "wb") as img_file:
                                img_file.write(image_data)
                            print(f"画像を抽出しました: {output_path}")
                        except Exception as write_e:
                            print(f"画像の書き込み中にエラー: {output_path}, {write_e}")
                    else:
                         print(f"警告: LTImage オブジェクトにストリームがありません ({image_name})")


except Exception as e:
    print(f"エラーが発生しました: {e}")

print(f"\n合計 {image_count} 個の画像要素が見つかりました。")

注意: このコードは基本的な例であり、すべてのPDFの画像を正しく抽出・保存できる保証はありません。特に、画像のフィルタ(圧縮形式)によっては、そのまま保存しても通常の画像ビューアで開けない場合があります(例: FlateDecode, CCITTFaxDecode, JBIG2Decode)。これらの画像を正しく表示するには、Pillowなどの画像処理ライブラリを使ってデコードや形式変換を行う必要があります。また、get_imagemeta() が期待通りに動作しないケースもあります。

  • 不正なPDF/破損したPDF: pdfminer.six は比較的堅牢ですが、不正な形式や破損したPDFファイルを処理しようとすると、予期せぬエラー(PDFSyntaxError, PSEOF など)が発生することがあります。必ず try...except ブロックで囲み、エラー処理を実装しましょう。
  • メモリ使用量: 大きなPDFファイルや、非常に複雑なレイアウトを持つPDFを解析する場合、特にレイアウト分析を詳細に行うと、大量のメモリを消費することがあります。必要に応じて、処理するページを分割したり、LAParams を調整して分析の粒度を変えたりすることを検討してください。
  • テキスト抽出の不完全さ: PDFの仕様上、文字の描画順序と論理的な読み順序が一致しない、特殊なエンコーディングが使われている、テキストが画像として埋め込まれている(OCRが必要)などの理由で、テキストが正しく抽出できない場合があります。
  • フォント情報: pdfminer.six は文字のフォント名やサイズを取得できますが、PDFにフォントが埋め込まれていない場合や、特殊なフォントマッピングが行われている場合、正確な情報が得られないことがあります。
  • 依存ライブラリ: pdfminer.six は暗号化されたPDFを扱うために、追加で cryptography ライブラリを必要とすることがあります(pip install pdfminer.six[crypto])。
  • 高レベル vs 低レベル API: 簡単なテキスト抽出であれば、extract_text などの高レベルAPIが手軽で効率的です。詳細なレイアウト分析や座標情報が必要な場合は低レベルAPIが必要になりますが、その分処理時間は長くなる傾向があります。
  • LAParamsの調整: LAParams の設定、特に boxes_flow やマージン関連のパラメータは、解析の精度だけでなく処理速度にも影響を与える可能性があります。デフォルト設定で問題なければ、無理に変更する必要はありません。
  • 必要な情報のみ取得: レイアウト分析が不要な場合は、LAParams を使わずにシンプルな TextConverter を使う、あるいは extract_textlaparams=None (またはデフォルト) にするなど、処理を軽量化することを検討します。

これらの高度な機能や注意点を理解することで、pdfminer.six をより効果的に、そして安全に活用することができるでしょう。👍

他のPDFライブラリとの比較 🆚

Pythonには、PDFを扱うためのライブラリが pdfminer.six 以外にもいくつか存在します。それぞれに得意なこと、不得意なことがありますので、目的に合わせて適切なライブラリを選択することが重要です。ここでは、代表的なライブラリと pdfminer.six を比較してみましょう。

ライブラリ 主な特徴・得意なこと pdfminer.six との比較 注意点
pdfminer.six
  • ✅ 詳細なテキスト抽出(座標、フォント情報)
  • ✅ 強力なレイアウト分析(テキストブロック、行、文字)
  • ✅ 多様な出力形式 (Text, HTML, XML)
  • ✅ 純粋なPython実装 (一部暗号化処理除く)
  • ➕ レイアウト分析能力が非常に高い
  • ➕ テキスト要素へのアクセスが詳細
  • ➖ PDFの編集・生成機能は基本的にない
  • ➖ 画像抽出は可能だが、形式変換は行わない
  • ➖ パフォーマンスが他のライブラリより劣る場合がある
純粋なPDF「読み取り・解析」に特化。
PyPDF2 / pypdf
(PyPDF2は開発終了、後継はpypdf)
  • ✅ PDFの分割・結合・回転・暗号化/復号化
  • ✅ ページ単位の操作
  • ✅ メタデータの読み書き
  • ✅ テキスト抽出(基本的なもの)
  • ✅ フォームフィールドのデータ抽出
  • ➕ PDFの操作・編集機能が豊富
  • ➕ ドキュメントレベルの操作が得意
  • ➖ テキスト抽出の精度やレイアウト分析能力は pdfminer.six に劣る
  • ➖ 文字の座標や詳細なフォント情報は取得しにくい
PDFファイルの構造的な操作や、簡単なテキスト抽出、メタデータ編集に向いている。2022年頃にPyPDF2の開発は停止し、pypdfがアクティブな後継ライブラリとなっている。
PyMuPDF (fitz)
  • ✅ 高速なテキスト・画像抽出
  • ✅ PDFのレンダリング(画像化)
  • ✅ 注釈(アノテーション)の追加・編集
  • ✅ PDFの編集(テキスト、画像の追加・削除)
  • ✅ OCR機能(別途Tesseractが必要な場合あり)
  • ✅ 他のドキュメント形式(XPS, EPUBなど)も一部対応
  • ➕ 非常に高速
  • ➕ レンダリングや編集機能が強力
  • ➕ 画像抽出・変換が得意
  • ➖ C言語ベースのライブラリ (MuPDF) のラッパーであり、純粋なPythonではない
  • ➖ 環境によってはインストールがやや煩雑な場合がある
  • ➖ レイアウト分析の詳細度は pdfminer.six ほどではない可能性がある
速度と多機能性(特にレンダリングや編集)を重視する場合に強力な選択肢。ライセンス (GNU AGPL) に注意が必要な場合がある。
ReportLab
  • ✅ PDFの生成・作成
  • ✅ 図形、テキスト、画像の描画
  • ✅ グラフ作成機能
  • ✅ 帳票やレポートの動的な生成
  • ➕ PDFを「作る」ことに特化
  • ➖ PDFの読み取り・解析機能は持たない
プログラムで動的にPDFドキュメントを生成したい場合に使うライブラリ。pdfminer.six とは目的が異なる。

目的によって最適なライブラリは異なります。

  • PDFから詳細なテキスト情報(座標含む)やレイアウト構造を正確に抽出したい場合:
    ➡️ pdfminer.six が最有力候補です。特に、表形式データの抽出や、特定の領域からのテキスト抽出など、レイアウトに依存する処理に適しています。
  • PDFのページを結合・分割したり、回転させたり、基本的なテキストをサッと抽出したい場合:
    ➡️ pypdf (旧 PyPDF2) が手軽で十分な機能を提供します。ドキュメントの構造的な操作が得意です。
  • とにかく高速にテキストや画像を抽出したい、PDFを画像として扱いたい、PDFに注釈を加えたり編集したりしたい場合:
    ➡️ PyMuPDF (fitz) が非常に強力です。多機能性と速度が求められる場合に適しています。
  • プログラムで動的にPDFファイルを生成したい場合:
    ➡️ ReportLab など、PDF生成に特化したライブラリを使用します。

場合によっては、これらのライブラリを組み合わせて使うことも有効です。例えば、pypdf でPDFを前処理(分割や復号化)し、pdfminer.six で詳細なテキストとレイアウトを解析する、といった連携も考えられます。

pdfminer.six は、その詳細な解析能力、特にレイアウト分析において、他のライブラリにはない強みを持っています。PDFの内容を深く理解し、構造化された情報を取り出したい場合に、非常に頼りになるライブラリと言えるでしょう。🧐

まとめ 🏁

このブログでは、PythonのPDF解析ライブラリ pdfminer.six について、その概要からインストール、基本的な使い方、主要な機能(テキスト抽出、レイアウト分析、画像抽出、目次抽出)、高度な使い方、そして他のライブラリとの比較まで、幅広く解説してきました。

pdfminer.six のポイントおさらい:
  • ✅ PDFからテキストや情報を抽出するための強力なPythonライブラリ。
  • ✅ 特にレイアウト分析機能が優れており、テキストの座標や構造(ブロック、行、文字)を詳細に把握できる。
  • ✅ コマンドラインツール (pdf2txt.py) で手軽にテキスト抽出が可能。
  • ✅ Pythonスクリプトから高レベルAPI (extract_text, extract_pages) や低レベルAPIを使い分けて、柔軟な処理を実現できる。
  • LAParams でレイアウト分析の挙動をカスタマイズ可能。
  • ✅ テキスト以外にも、画像データや目次情報、PDFの低レベルオブジェクトへのアクセスも提供。
  • ✅ 純粋なPython実装(一部除く)で、導入が比較的容易。

pdfminer.six を使いこなせば、これまで手作業で行っていたPDFからの情報抽出作業を自動化したり、PDF内のデータを分析して新たな知見を得たりすることが可能になります。特に、請求書、レポート、論文など、構造化された情報を含むPDFを扱う際に、その真価を発揮するでしょう。

もちろん、PDFというフォーマットの複雑さ故に、常に完璧な結果が得られるとは限りません。しかし、pdfminer.six が提供する豊富な機能と詳細な情報は、多くの課題を解決するための強力なツールとなります。

ぜひ、この記事を参考に pdfminer.six を試してみて、あなたのPDF活用術をレベルアップさせてください! Happy PDF Mining! ⛏️😄

より詳しい情報や最新のアップデートについては、公式ドキュメントGitHubリポジトリを参照することをお勧めします。

コメント

タイトルとURLをコピーしました