2020-12-11に更新

[Python]PDFから特定の文字列を抽出&ページを画像として保存するツール

はじめに

ノンプロ研」というコミュニティで開催されている講座で使用する目的で作りました。
毎回講座の資料が講師の方から共有してもらえるので、それを演習問題のページだけを抜き出して画像保存するツールです。

PDF資料から特徴のあるページだけを抽出するような作業を行いたい場合は、抽出キーワードを変えると使えるかもしれません。

ただどうも文字抽出が上手くできるかどうかは元のPDFファイルによるものが多く、必ずしもできる訳ではなさそうなので注意が必要です。

使い方

  • 必要なライブラリのインストールをしておいてください(次の項目参照)。
  • 準備したい講座回数(NO)、資料PDFファイルの名(FILES)とフォルダパス(PDF_DIRPATH)、画像出力先フォルダのパス(OUTPUT_DIRPATH)をスクリプトに記入してください
  • スクリプトを実行して少し待つと、該当する講座の資料から演習ページだけ画像の保存が行われます(pngファイル)
  • 同時に演習をテキストで抜き出したものがテキストファイルで保存されます

image

使用しているPythonライブラリについて

  • PDFから文字列を抽出するpdfminer.six
  • PDFを画像保存するpdf2image

これらを使っています。
またpdf2image を使うには、依存関係のあるpillowPopplerというライブラリも必要のようです。

スクリプトの解説

(1)ライブラリのインポート

import pathlib
import re
from io import StringIO

from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
import pdf2image

(2)ユーザー入力項目

このツールを使用する前に、変換したいPDFファイルやディレクトリをユーザー自身で指定します。
ユーザーが入力する部分は、グローバルな定数としてまとめてあります。
(パス取得のスクリプトがまだやや冗長な気がしています)

# ユーザー入力項目
# 準備したい講座回数・PDFファイル名をリストに記入
NO = 3
FILES = [
    '20200923初心者講座Pythonコース-01.pdf',
    '20200930初心者講座Pythonコース-02.pdf',
    '20201007初心者講座Pythonコース-03.pdf',
]
# 資料PDFファイルが保存されているフォルダのパスを記入
PDF_DIRPATH = pathlib.Path(
    '<pdfファイルのあるフォルダのパス>'
    )
PDF_FILEPATH = PDF_DIRPATH / FILES[NO-1]
# 出力先フォルダのパス
OUTPUT_DIRPATH = pathlib.Path(
    '<変換した画像を保存したいフォルダのパス>'
    )
# 抽出する文字列(正規表現:ここでは「演習○-○」)
SEARCH_TEXT = r'演習\s*\d-\d+'

Python標準ライブラリのpathlibを使って、パスを記述しています。
pathlib.Path(<パス文字列>)とすることで、Pathオブジェクトを作成しています。

このPathオブジェクトを活用すると、パス同士を/記号を使って結合することができます。
ちなみにこのスクリプトではPDF_DIRPATHがPathオブジェクト・PDF_DIRPATHはstr型のオブジェクトですが、このように一方がPathオブジェクトなら、文字列が混ざっていても/でパスとして結合することもできるようですね。

(参考)pathlibモジュールとosモジュール

どちらもPython標準ライブラリです。
Python3.4以前ではファイルパス操作にos.pathglobモジュールを使っていたようですが、
Python3.4でpathlibモジュールが追加されてからは、こちらを使う方がシンプルに書きやすくなったようです。

(3)正規表現にマッチするページとテキストを抽出する関数

PDFファイルから、(2)で指定した「抽出する文字列」(ここでは演習問題)に該当するページを、ページ数とテキストとして抽出する関数を作ります。
ここで画像からテキスト抽出できるpdfminer.sixライブラリを活用しています。

長い関数ですが、戻り値としてはoutput_texts変数で、これは「キーにページ番号、値に演習問題ナンバーとテキストが格納されたdict」となっています。

def get_text_from_pdf():
    """PDFファイルから演習問題に該当するテキストをページ数と共に抽出する関数
    Returns:
        output_texts(dict): 演習問題一覧。ページ数をキーとして、演習ナンバーと本文が入った辞書
    """
    # pdfminerの設定
    rsrcmgr = PDFResourceManager()
    codec = 'utf-8'
    laparams = LAParams()
    laparams.detext_vertical=True

    # PDFファイルを1ページずつ見て該当するかチェック
    output_texts = {}  # 該当するページ数とテキストを格納するdict
    with open(PDF_FILEPATH, 'rb') as fp:
        for i, page in enumerate(PDFPage.get_pages(fp)):
            outfp = StringIO()
            device = TextConverter(
                rsrcmgr,
                outfp,
                codec=codec,
                laparams=laparams
                )
            interpreter = PDFPageInterpreter(rsrcmgr, device)
            interpreter.process_page(page)

            extracted_text = outfp.getvalue() \
                .replace('\u2028', '') \
                .split('Copyright')[0] \
                .rstrip()

            # ページ抽出:抽出条件(演習問題のページ)に該当すればTrue
            extracted_page = re.search(SEARCH_TEXT, extracted_text)

            # 演習問題のページ番号・演習番号・テキストを格納したdict作成
            if extracted_page:
                output_texts[i+1] = (extracted_page.group(), extracted_text)
    return output_texts

処理としては
「PDFファイルを1ページずつ見ていって、SEARCH_TEXTに一致するページが出てきたら、ページ番号とテキストを変数output_textsに格納する」
というような処理を行っています。

PDFMinerについて

仕組みやスクリプトをを完璧に理解しきれていませんが、こちらの記事を参考にさせていただきました。
出来上がったスクリプトでは、for文の外と中にそれぞれ設定項目を分けて記述しています(かえってわかりづらくしてしまっているかもしれません)。

PDFファイルから文章を抽出する処理の流れは結構複雑で、そのまま抽出することはできないようです。
PDFファイルからPDFParserを使ってPDFDocumentという形式に一旦変換して、そこからPDFInterpreter, PDFDeviceを使って翻訳し、テキストとして出力する、という流れかと思います。この後半の過程で、PDFResouceManagerにフォントやイメージなどのリソース情報を保管しています。
(間違ってたらすみません)

こちらの記事に公式ドキュメントから引用された図解があるので、公式ドキュメントと合わせて参考にさせていただきました。

(4)抽出したページのテキストをファイルに保存する関数

(3)で作ったoutput_textsをそのままテキストファイルに出力する関数です。
ただdict型の変数をそのまま出力しているだけなので、かなり見づらいテキストファイルになっています。
整形して出力するようにしたいところ。

def save_text(output_texts):
    """演習問題一覧をテキストファイル出力
    Arg:
        output_texts(dict): 演習問題一覧。ページ数をキーとして、演習ナンバーと本文が入った辞書
    """
    output_text_path = OUTPUT_DIRPATH / f'0{NO}_ensyu.txt'
    with output_text_path.open('w') as f:
        print(output_texts, file=f)

print()file引数にここではテキストファイルのパスを指定することで、出力先をファイルに指定しています。

(5)抽出したページの画像を保存する関数

PDFファイルを1ページずつ見ていって、抽出したいページだけを画像ファイルに保存する関数です。
ここでpdf2imageライブラリを使用しています
(3)で作ったoutput_texts(dict型でした)に抽出したいページ数がキーとして入っているので、これを用いて抽出したいページかどうかを判定しています。

def save_images(output_texts):
    """演習問題を画像ファイル出力
    Arg:
        output_texts(dict): 演習問題一覧。ページ数をキーとして、演習ナンバーと本文が入った辞書
    """
    # - convert_from_path関数には1ページごとの画像のリストが入る
    # - mkidr(exist_ok=True)にすると上書きされるっぽい
    img_dirpath = OUTPUT_DIRPATH / f'0{NO}'
    pathlib.Path(img_dirpath).mkdir(exist_ok=True)

    images = pdf2image.convert_from_path(PDF_FILEPATH, size=1280)
    for i, image in enumerate(images):
        if i+1 in contents:
            filename = contents[i+1][0].replace(' ', '')
            image_filepath = img_dirpath / f'{filename}.png'
            image.save(image_filepath)

pdf2imageについて

扱い方は簡単で、pdf2image.convert_from_path()の引数にPDFファイルパスを指定してやれば良さそうです。
ここではsize引数でサイズを指定してみました。他の引数を使えば、pngファイルだけでなくjpgファイルなどファイル形式を指定することもできるようです。

(6)実行

以下で実行します。
PDFファイルのページ数によっては、少し時間がかかってしまいます。

# 実行
contents = get_text_from_pdf()
save_text(contents)
save_images(contents)

参考にしたサイト

pdf2imageについて
PythonでPDFをまとめて画像に変換する - Qiita

PDFMinerについて
【Python】PDFの文章をページ毎にCSVに変換(2/24追記) - Qiita
【PDFMiner】PDFからテキストの抽出 - Qiita

おわりに

PDFを操作するのをやってみたくて、用途はないけど色々調べてたのが役に立ちました。
今回はシンプルなPDFファイルだったのでまだ良かったですが、複雑なレイアウトのPDFファイルを扱いたい場合はもっと泥臭い作業になるのかな…。

ライブラリの使い方はあまり理解してないので、ちゃんと整理しておこうと思います。

そして今回このツール制作を通じての課題。
使い勝手が良くメンテナンス性の高いスクリプトにするにはどうすれば良いんだろう? というところに興味があります。
サーバーにアップして誰でも使えるツールを作る…というのはちょっと難しく労力が大きいので、ローカルで実行すればすぐに使えるような形を意識してみました。

ユーザーが入力するべきところ、変更の可能性があるところ(抽出する文字列)を定数としてグローバルスコープに書いているのですが、もっとスマートなやり方があるのかな、と思っています。
関数の使い方についても学んでいきたいです。

MEMO:スクリプト全文

スクリプト全文はこちらに載せました!
mytools/pdf_to_pic.py at master · Massasquash/mytools · GitHub

ツイッターでシェア
みんなに共有、忘れないようにメモ

Massa

北海道でアプリ制作に取り組んでるノンプログラマな農夫。仕事や日常生活で感じる小さな不便を解消すべく趣味と実益を兼ねて遊んでます ■Python・GAS + LINE bot

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント