【Python】PDFのテキストを抽出し、いろんな情報と共にCSV出力する

Pythonではスクレイピングができますが、今回はPDFファイルの文字を読み取るプログラムを作成していきます。

テキストの読み取りだけでなく、テキストの座標やページ番号なども併せてCSVファイルとして出力していきます。

PDFが画像ベースの場合(PDF上で文字を選択できない場合)は、こちらの記事を参考にしてください

コード全容

最初に、コード全体を掲載しておきます。

プログラム作成にあたり、以下の記事を大いに参考にさせていただきました(以下記事、わかりやすいです)。

https://qiita.com/fumitrial8/items/f3d92fca0de409feaee9

また今回は、IPAの以下のPDFからテキストを抽出しました。

https://www.ipa.go.jp/files/000059695.pdf

from pdfminer.converter import PDFPageAggregator
from pdfminer.layout import LAParams, LTContainer, LTTextBox
from pdfminer.pdfinterp import PDFPageInterpreter, PDFResourceManager
from pdfminer.pdfpage import PDFPage

import pandas as pd


def find_textboxes(layout):
    if isinstance(layout, LTTextBox):
        return [layout]
    elif isinstance(layout, LTContainer):
        boxes = []
        for child in layout:
            boxes.extend(find_textboxes(child))
        return boxes
    else:
        return []
        
    
with open("000059695.pdf", "rb") as f:
    
    pdfPages = PDFPage.get_pages(f)
    
    #文字読み取りのルール指定
    laParams = LAParams(line_overlap = 0.5,
                        word_margin  = 0.1,
                        char_margin  = 2,
                        line_margin  = 0.5,
                        detect_vertical = True)
    
    #共有のリソースを管理するリソースマネージャー作成
    resourceManager = PDFResourceManager()
    #ページ集約
    device = PDFPageAggregator(resourceManager, laparams=laParams)
    #インタプリタオブジェクト作成
    interpreter = PDFPageInterpreter(resourceManager, device)
    
    df = pd.DataFrame()
    
    #ページごとに処理
    
    for page_no, page in enumerate(pdfPages, start=1):
        #ページ処理
        interpreter.process_page(page)
        #LTPageオブジェクトを取得
        layout = device.get_result()
        #1ページ内のテキストのまとまりのリストを取得
        boxes = find_textboxes(layout)
        
        #テキストひとまとまりごとに処理
        for box in boxes:
            df_page = pd.DataFrame({"x_start":[box.x0],
                                    "x_end"  :box.x1,
                                    "y_start":box.y0,
                                    "y_end"  :box.y1,
                                    "text"   :box.get_text().strip(),
                                    "page"   :page_no}
                                    )
            
            df = df.append(df_page)

df = df.reset_index(drop=True)
with open("output.csv", mode="w", encoding="cp932", errors="ignore", newline="") as f:
    df.to_csv(f)

このコードで出力されるCSVは以下のようなものです。

x_startやx_endはテキスト領域の座標を示しています。詳しくは後述します。

pdfminerをインストール

PDFの読み取りにはpdfminerというライブラリを使用しています。まずは、pdfminerをインストールします。anacondaの場合は以下コマンドを実行します。

conda install -c conda-forge pdfminer

LAParamsで文字の抽出ルールを指定

 laParams = LAParams(line_overlap = 0.5,
                        word_margin  = 0.1,
                        char_margin  = 2,
                        line_margin  = 0.5,
                        detect_vertical = True)

LAParamsでは1つのブロックとみなす文字間隔等のルールを指定します。文字がうまく読み取れない場合は、このパラメータをいじることで改善されるかもしれません。

基本的に、char_margin、word_marginとline_marginを調整します。line_overlapは、上下で文字列が重なった際に、1行とみなされないようにするためのパラメータです(基本デフォルトの0.5でOK)。

縦書きの文書を読み取る場合はdetect_verticalをTrueにします。

1ページずつ処理

PDFからのテキスト抽出は、1ページずつ行います。処理の全容は以下の通りです。

for page_no, page in enumerate(pdfPages, start=1):
        #ページ処理
        interpreter.process_page(page)
        #LTPageオブジェクトを取得
        layout = device.get_result()
        #1ページ内のテキストのまとまりのリストを取得
        boxes = find_textboxes(layout)
        
        #テキストひとまとまりごとに処理
        for box in boxes:
            df_page = pd.DataFrame({"x_start":[box.x0],
                                    "x_end"  :box.x1,
                                    "y_start":box.y0,
                                    "y_end"  :box.y1,
                                    "text"   :box.get_text().strip(),
                                    "page"   :page_no}
                                    )
            
            df = df.append(df_page)

boxesでは、ページ内に存在するテキストのまとまりのリストを生成します。テキストのまとまりとは、出力されるCSVファイルの1行分に相当するものです。

boxesの要素は以下のようなものが含まれています(for文中のboxに相当)。

boxes[1]
>> <LTTextBoxHorizontal(1) 56.102,715.973,478.258,735.932 '情報セキュリティ早期警戒パートナーシップの紹介\n'>

テキストのみを取得するには、box.get_text()を用います。上記例に座標のような数値が出力されていますが、これはテキストのまとまりがページ上のどの位置にあるかを示す座標です。

テキストの座標情報

座標情報は4種類あります。「情報セキュリティ早期警戒パートナーシップの紹介」というテキストを例にその関係性を示します。

座標情報の概要

box.bbox で4つの座標のタプルを取得することができます。また、box.x0 やbox.y1のように個々の座標を取得することもできます。

box.bbox
>>(56.1025, 715.9725199999999, 478.257688, 735.93236)

box.x0
>> 56.1025

box.y1
>> 735.93236

データフレームに変換

最後はデータフレームにして出力します。必要な情報を列として追加し、全ページ文集約して出力します。ページ番号はfor文を利用して取得しています。

出力時、単純にdf.to_csv()とすると、cp932のコーデックエラーが発生したので、以下のようにしました。日本語は本当に扱いづらい^^;

with open("output.csv", mode="w", encoding="cp932", errors="ignore", newline="") as f:
    df.to_csv(f)

まとめ

PythonでPDFから文字を読み取り、各種情報をCSVファイルとして出力する方法を紹介しました。座標情報を上手く使えば、表の読み取りなんかもできます。

抽出したテキストは解析に使うことで、作業の自動化とか効率化とかいろんな可能性がありそうです。

ではでは👋