🔥

【Python】PyMuPDFはここまでできる!PDFから文字の位置・フォント情報まで根こそぎ抜き出す方法

に公開

【Python】PyMuPDFはここまでできる!PDFから文字の位置・フォント情報まで根こそぎ抜き出す方法

はじめに

PDFからテキストを抽出する際、Pythonライブラリの PyMuPDF (fitz) を使っている方は多いのではないでしょうか。page.get_text() で簡単にテキストが取れるのは便利ですよね。

でも、PyMuPDFの真の力はそんなものではありません。実は、**「どのページのどの位置に」「どんなフォントや大きさで」「どの文字が」**書かれているかといった、超詳細な情報を根こそぎ取得する能力を秘めています。

この記事では、基本的なテキスト抽出から一歩進んで、PyMuPDFが持つ強力なテキスト抽出オプション、特に文字レベルの情報を取得できる get_text("rawdict") の使い方と、その応用例として「画像に含まれるテキストだけを効率的にOCRする方法」を、具体的なコードと共に徹底解説します。

この記事を読めば、あなたは以下のことができるようになります。

  • PDF内のテキストを、段落や単語単位で構造的に取得する
  • 個々の文字の正確な位置(座標)やバウンディングボックスを取得する
  • 文字ごとにフォント名、サイズ、色といったスタイル情報を取得する
  • これらの詳細情報を活用して、ドキュメントのレイアウト解析や、RAGのための高度な前処理を行う
  • PDF内の画像領域だけを狙って効率的にOCRをかける

準備:テスト用PDFファイルのダウンロード

今回のサンプルコードでは、以下のURLで公開されているPDFファイルを使用します。コード内で自動的にダウンロードする処理を加えています。

【重要】著作権に関するご注意
※上記のPDFファイルは、本コードの動作確認を目的としてサンプルとして使用しています。著作権はファイルの発行元に帰属しますので、取り扱いには十分ご注意いただき、本来の目的以外での利用は避けてください。

なぜPyMuPDFはこんなに高速で高機能なのか?

[cite_start]少しだけ内部的な話をすると、PyMuPDFは純粋なPythonライブラリではなく、非常に高性能なC言語製ライブラリ「MuPDF」をPythonから使えるようにしたものです [cite: 21][cite_start]。そのため、他のライブラリと比較して数倍から数十倍という圧倒的な処理速度を誇ります [cite: 21]。単に機能が豊富なだけでなく、そのパフォーマンスが実用性を支えているのです。

[cite_start]ちなみに、ライブラリ名はPyMuPDFですが、歴史的な経緯からimport fitzとして使うので覚えておきましょう [cite: 22]。


📄 基本から応用まで!page.get_text() の抽出オプション

[cite_start]テキスト抽出の主役は page.get_text() メソッドです。このメソッドは、引数に指定する文字列(オプション)によって、出力の形式と情報の粒度を自在に変えることができます [cite: 30]。

1. プレーンテキスト ("text")

まずは基本のプレーンテキスト抽出です。多くの方がこの方法を使っているでしょう。
[cite_start]一つ重要なのが sort=True という引数。これを指定することで、PDF内部の順序ではなく、人間が読む自然な順序(上から下、左から右)でテキストを取得できます [cite: 31]。

Note: requestsライブラリが必要です。インストールされていない場合は、pip install requests を実行してください。

import fitz  # PyMuPDF
import requests
import os

# --- テスト用PDFの準備 ---
url = "https://nihon-ped-surg.jp/wp-content/uploads/2021/11/pdf_test.pdf"
pdf_filename = "pdf_test.pdf"

# ファイルが存在しない場合のみダウンロード
if not os.path.exists(pdf_filename):
    print(f"テスト用PDFをダウンロードします: {url}")
    response = requests.get(url)
    with open(pdf_filename, "wb") as f:
        f.write(response.content)
    print(f"'{pdf_filename}' をダウンロードしました。")

# --- PDF処理 ---
try:
    doc = fitz.open(pdf_filename)
    page = doc[0]  # 最初のページを取得

    # シンプルなテキスト抽出(読みやすい順序で)
    text = page.get_text("text", sort=True)
    print("--- 抽出したプレーンテキスト ---")
    print(text)

    doc.close()
except Exception as e:
    print(f"エラー: {e}")
--- 抽出したプレーンテキスト ---
PDF のアップロードテスト用に作成しました
テスト用PDF
作成者

見出し 1
(このテキストのような) プレースホルダー テキストをタップして入力するだけで、すぐに作成を
開始できます。
    • PC、タブレット、スマートフォンから Word を使ってこの文書を表示、編集できます。
    • テキストの編集が可能で、画像、図形、表などのコンテンツの挿入も簡単です。
      Windows、Mac、Android、iOS デバイスから Word を使ってクラウドにシームレスに文
   書を保存できます。

    • 2ページ目も用意してあります。

2. ブロック単位 ("blocks")

テキストを段落のような「ブロック」単位で取得したい場合は、

"blocks" を使います。各ブロックのバウンディングボックス(BBox)、テキスト内容、ブロック番号などがタプルのリストとして返ってきます 。レイアウト解析の第一歩として非常に便利です。

import fitz

try:
    doc = fitz.open("pdf_test.pdf")
    page = doc[0]

    # ブロック単位でテキストを抽出
    blocks = page.get_text("blocks", sort=True)
    for block in blocks:
        # block[0:4] にBBox(x0, y0, x1, y1)が、block[4]にテキスト block[5] に番号が入っている
        print(f"block No.: {block[5]}")
        print(f"BBox: {block[0:4]}")
        print(f"Text: {block[4].strip()}")
        print("-" * 20)

    doc.close()
except Exception as e:
    print(f"エラー: {e}")

--- 実行結果 ---

block No.: 0
BBox: (67.0250015258789, 65.27403259277344, 352.6499938964844, 82.98603057861328)
Text: PDF のアップロードテスト用に作成しました
--------------------
block No.: 1
BBox: (67.0250015258789, 102.35198974609375, 225.10000610351562, 133.34799194335938)
Text: テスト用PDF
--------------------
block No.: 2
BBox: (67.0250015258789, 148.8579864501953, 106.05000305175781, 162.14198303222656)
Text: 作成者
--------------------
block No.: 3
BBox: (513.969970703125, 476.4580078125, 516.969970703125, 489.74200439453125)
Text: 
--------------------
block No.: 4
BBox: (67.0250015258789, 505.68902587890625, 147.5500030517578, 528.93603515625)
Text: 見出し 1
--------------------

... (中略) ...

3. 単語単位 ("words")

さらに細かく、単語単位でBBoxと共に取得したい場合は

"words" を使います 。特定のキーワードがページのどの位置にあるかを探したり、特定の矩形領域内のテキストだけを抽出したりする際に強力です。

import fitz

try:
    # PDFファイルを開きます。存在しない場合はエラーになります。
    doc = fitz.open("pdf_test.pdf")
    page = doc[0]

    # 単語単位でテキストを抽出します。
    words = page.get_text("words")
    # wordsは (x0, y0, x1, y1, "単語", block_no, line_no, word_no) のタプルのリストです。

    # 最初の10単語を番号付きで表示します。
    for i, word in enumerate(words[:10], 1): # 1から始まる連番を付けます。
        print(f"Word {i}: '{word[4]}', BBox: {word[0:4]}")

    doc.close()
except FileNotFoundError:
    print("エラー: 'pdf_test.pdf' が見つかりません。")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

--- 実行結果 ---

Word 1: 'PDF', BBox: (67.0250015258789, 65.27403259277344, 96.4170150756836, 82.98603057861328)
Word 2: 'のアップロードテスト用に作成しました', BBox: (99.80000305175781, 65.78602600097656, 348.37603759765625, 81.78602600097656)
Word 3: 'テスト用PDF', BBox: (67.0250015258789, 102.35198974609375, 218.18202209472656, 133.34799194335938)
Word 4: '作成者', BBox: (67.0250015258789, 149.2419891357422, 103.0250015258789, 161.2419891357422)
Word 5: '見出し', BBox: (67.0250015258789, 506.36102294921875, 125.34200286865234, 527.3610229492188)

... (中略) ...

💎【本題】文字レベルの情報を根こそぎ取得する "rawdict"

ここからが本題です。PyMuPDFの真骨頂とも言えるのが、

"dict" およびその上位版である "rawdict" オプションです。これらはページの全情報をネストされたPython辞書として返し、文字レベルでの詳細な分析を可能にします 。

"rawdict""dict" の全情報に加え、個々の文字に関する情報を含んでいるため、究極の詳細度を求めるならこちらを使います 。

"rawdict" の階層構造

"rawdict" の出力は、PDFの構造を反映した以下のような階層になっています 。

  • Page: ページ全体
    • Block: テキストの段落や画像などのかたまり
      • Line: テキストの1行
        • Span: 同じフォント・サイズ・色を持つ、連続した文字の集まり
          • Char: 個々の文字

文字 (Char) から取得できる情報

"rawdict" を使うと、Span の中に chars というキーで文字情報のリストが入っています。各 char 辞書から、以下のような驚くほど詳細な情報を取得できます 。

  • 'c': 文字そのもの (例: 'A')
  • 'origin': 文字のベースラインの開始座標 (x, y)
  • 'bbox': 文字を囲むバウンディングボックス (x0, y0, x1, y1)
  • 'font': フォント名 (例: 'Helvetica-Bold') 。
  • 'size': フォントサイズ 。
  • 'color': sRGB形式のフォント色 。

これらの情報を組み合わせることで、「このPDFのこの見出しは、どのフォントでどのくらいの大きさか?」といった分析や、文書の構造をプログラムで理解することが可能になります。

これは、高度なデータ抽出やRAG(Retrieval-Augmented Generation)のための前処理として非常に強力な武器となります 。

サンプルコード: 文字レベルの情報を抽出する

import fitz

try:
    doc = fitz.open("pdf_test.pdf")
    page = doc[0]

    # "rawdict" オプションで詳細な情報を取得
    raw_data = page.get_text("rawdict")

    # 階層をたどって文字情報を表示
    for block in raw_data['blocks']:
        if block['type'] == 0:  # テキストブロックのみを対象
            for line in block['lines']:
                for span in line['spans']:
                    font_size = span['size']
                    font_name = span['font']
                    print(f"\n--- Span --- Font: {font_name}, Size: {font_size:.2f}")
                    for char in span['chars']:
                        # char には c, origin, bbox が含まれる
                        char_text = char['c']
                        char_bbox = char['bbox']
                        print(f"  Char: '{char_text}', BBox: ({char_bbox[0]:.2f}, {char_bbox[1]:.2f}, {char_bbox[2]:.2f}, {char_bbox[3]:.2f})")

    doc.close()
except Exception as e:
    print(f"エラー: {e}")

--- 実行結果 ---

--- Span --- Font: TimesNewRomanPSMT, Size: 16.00
  Char: 'P', BBox: (67.03, 65.27, 75.92, 82.99)
  Char: 'D', BBox: (76.02, 65.27, 87.57, 82.99)
  Char: 'F', BBox: (87.52, 65.27, 96.42, 82.99)

--- Span --- Font: MS-PMincho, Size: 16.00
  Char: ' ', BBox: (96.42, 65.79, 99.80, 81.79)
  Char: 'の', BBox: (99.80, 65.79, 114.98, 81.79)
  Char: 'ア', BBox: (115.03, 65.79, 129.29, 81.79)
  Char: 'ッ', BBox: (129.29, 65.79, 141.03, 81.79)
  Char: 'プ', BBox: (140.78, 65.79, 155.90, 81.79)
  Char: 'ロ', BBox: (156.01, 65.79, 168.57, 81.79)
  Char: 'ー', BBox: (168.50, 65.79, 184.50, 81.79)
  Char: 'ド', BBox: (184.50, 65.79, 195.82, 81.79)
  Char: 'テ', BBox: (195.75, 65.79, 210.18, 81.79)
  Char: 'ス', BBox: (210.23, 65.79, 223.35, 81.79)
  Char: 'ト', BBox: (223.46, 65.79, 232.90, 81.79)
  Char: '用', BBox: (232.95, 65.79, 248.95, 81.79)
  Char: 'に', BBox: (248.95, 65.79, 264.14, 81.79)
  Char: '作', BBox: (264.18, 65.79, 280.18, 81.79)
  Char: '成', BBox: (280.18, 65.79, 296.18, 81.79)
  Char: 'し', BBox: (295.93, 65.79, 308.36, 81.79)
  Char: 'ま', BBox: (308.41, 65.79, 321.59, 81.79)
  Char: 'し', BBox: (321.40, 65.79, 333.83, 81.79)
  Char: 'た', BBox: (333.88, 65.79, 348.38, 81.79)

--- Span --- Font: TimesNewRomanPSMT, Size: 16.00
  Char: ' ', BBox: (348.65, 65.27, 352.65, 82.99)

--- Span --- Font: MS-PMincho, Size: 28.00
  Char: 'テ', BBox: (67.03, 103.25, 92.28, 131.25)
  Char: 'ス', BBox: (92.28, 103.25, 115.24, 131.25)

... (中略) ...

まとめ

今回は、PyMuPDFを使ってPDFからテキスト情報をより深く、より詳細に抽出する方法を解説しました。

get_text() のオプション ("text", "blocks", "words") を使い分けることで、必要な粒度で構造化されたテキストを取得できる。

究極のオプション "rawdict" を使えば、文字単位の座標・フォント・サイズといった詳細な情報までアクセスできる。

画像のBBoxを取得し、clip パラメータとOCRを組み合わせることで、画像中のテキストを極めて効率的に抽出できる。

PyMuPDFは、単にPDFのテキストを読むだけのライブラリではありません。ドキュメントの構造を理解し、意味のある情報を抽出するための強力な分析ツールです。

ぜひ、皆さんの開発プロジェクトでもその真価を引き出してあげてください。

Discussion