「ノンプロ研」というコミュニティで開催されている講座で使用する目的で作りました。
毎回講座の資料が講師の方から共有してもらえるので、それを演習問題のページだけを抜き出して画像保存するツールです。
PDF資料から特徴のあるページだけを抽出するような作業を行いたい場合は、抽出キーワードを変えると使えるかもしれません。
ただどうも文字抽出が上手くできるかどうかは元のPDFファイルによるものが多く、必ずしもできる訳ではなさそうなので注意が必要です。
NO
)、資料PDFファイルの名(FILES
)とフォルダパス(PDF_DIRPATH
)、画像出力先フォルダのパス(OUTPUT_DIRPATH
)をスクリプトに記入してくださいpdfminer.six
pdf2image
これらを使っています。
またpdf2image
を使うには、依存関係のあるpillow
とPoppler
というライブラリも必要のようです。
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
このツールを使用する前に、変換したい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オブジェクトなら、文字列が混ざっていても/
でパスとして結合することもできるようですね。
どちらもPython標準ライブラリです。
Python3.4以前ではファイルパス操作にos.path
やglob
モジュールを使っていたようですが、
Python3.4でpathlib
モジュールが追加されてからは、こちらを使う方がシンプルに書きやすくなったようです。
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
に格納する」
というような処理を行っています。
仕組みやスクリプトをを完璧に理解しきれていませんが、こちらの記事を参考にさせていただきました。
出来上がったスクリプトでは、for文の外と中にそれぞれ設定項目を分けて記述しています(かえってわかりづらくしてしまっているかもしれません)。
PDFファイルから文章を抽出する処理の流れは結構複雑で、そのまま抽出することはできないようです。
PDFファイルからPDFParserを使ってPDFDocumentという形式に一旦変換して、そこからPDFInterpreter, PDFDeviceを使って翻訳し、テキストとして出力する、という流れかと思います。この後半の過程で、PDFResouceManagerにフォントやイメージなどのリソース情報を保管しています。
(間違ってたらすみません)
こちらの記事に公式ドキュメントから引用された図解があるので、公式ドキュメントと合わせて参考にさせていただきました。
(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
引数にここではテキストファイルのパスを指定することで、出力先をファイルに指定しています。
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.convert_from_path()
の引数にPDFファイルパスを指定してやれば良さそうです。
ここではsize
引数でサイズを指定してみました。他の引数を使えば、pngファイルだけでなくjpgファイルなどファイル形式を指定することもできるようです。
以下で実行します。
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ファイルを扱いたい場合はもっと泥臭い作業になるのかな…。
ライブラリの使い方はあまり理解してないので、ちゃんと整理しておこうと思います。
そして今回このツール制作を通じての課題。
使い勝手が良くメンテナンス性の高いスクリプトにするにはどうすれば良いんだろう? というところに興味があります。
サーバーにアップして誰でも使えるツールを作る…というのはちょっと難しく労力が大きいので、ローカルで実行すればすぐに使えるような形を意識してみました。
ユーザーが入力するべきところ、変更の可能性があるところ(抽出する文字列)を定数としてグローバルスコープに書いているのですが、もっとスマートなやり方があるのかな、と思っています。
関数の使い方についても学んでいきたいです。
スクリプト全文はこちらに載せました!
mytools/pdf_to_pic.py at master · Massasquash/mytools · GitHub
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント