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ファイルとして出力する方法を紹介しました。座標情報を上手く使えば、表の読み取りなんかもできます。
抽出したテキストは解析に使うことで、作業の自動化とか効率化とかいろんな可能性がありそうです。
ではでは👋