はむ吉(のんびり)の練習ノート

プログラミングとことばに関する話題を中心に,思いついたこと,試してみたこと,学んだことを,覚え書きを兼ねてまとめます.その際に役立った,技術書,参考書,辞書,機器などの紹介も行います.

カレントディレクトリにある複数の PDF 文書をファイル名順に Python の PyPDF2 で結合する:コンテキストマネージャ自作の学習を兼ねて

複数の PDF ファイルがあり,これを一定の規則で結合したい場合があります.規則が単純ならば既存のソフトウェアで対応できますが,そうでなければ自分でスクリプトを書いたほうが円滑です.典型例として,カレントディレクトリの PDF ファイルをモジュール PyPDF2 を利用してその名称順に結合する Python スクリプトを作成したので,備忘録を兼ねて簡単な説明とともに記しておきます.その際に,コンテキストマネージャを自作する方法の学習を兼ねて「ひとひねり」を加えました.

言い訳

忘れないうちにと急遽作成したため,本記事は荒削りです.特に,後半に記したコンテキストマネージャについての解説については,現時点における直感的な理解を書きつけただけで,正確性に欠ける部分が多くあります.後で改稿するかもしれません.

環境

使用した環境は Windows 10 上の Python 3.7.0 (Anaconda) です.PyPDF2 が未導入の場合,pip install PyPDF2 などで導入してください.

前提

  • A4 サイズの PDF ファイル *.pdf がカレントディレクトリに複数あるので,これらを一つのファイル output.pdf に統合したい.
  • 結合する順番は,ファイル名の昇順とする.
  • 追加の目標として,コンテキストマネージャの自作について学習する.

配布物

以下に示す Python スクリプトと,結合を試すのに使えるダミーの PDF ファイル群をまとめたものを,以下のページで配布します.
www.dropbox.com

スクリプト

以下のスクリプトを PDF ファイル群のあるディレクトリに配置し,これを実行すると,上記の前提に従って結合が行われます.

import contextlib
import PyPDF2
import os


# PdfFileMerger を使用後,close する手間を省くためのファクトリ関数
@contextlib.contextmanager
def get_merger(*args, **kwargs):
    merger = PyPDF2.PdfFileMerger(*args, **kwargs)
    yield merger
    merger.close()


# カレントディレクトリにある PDF ファイルを列挙する
input_files = [f for f in os.listdir(".") if f.endswith(".pdf")]
with get_merger() as merger:  # PDF 結合用のオブジェクトを得る
    for f in input_files:
        merger.append(f)  # 列挙したファイルを一つずつ追加する
    merger.write("output.pdf")  # 全文追加したら出力する
# merger.close() は 不要

上記スクリプトでは,PyPDF2 の提供する PdfFileMerger オブジェクトを用いて,PDF ファイルを結合しています.上記ではコンテキストマネージャがらみの記述があり複雑ですが,それを取り払うと,実は以下のように簡潔になります.安全性をあまり気にしなければ,これでも十分動きます.

import PyPDF2
import os


input_files = [f for f in os.listdir(".") if f.endswith(".pdf")]
merger = PyPDF2.PdfFileMerger()
for f in input_files:
    merger.append(f)
merger.append("output.pdf")
merger.close()  # 問題はこれ

ただし,上記の簡易版では,用が済んだ後に close() を行うことで,明示的にファイルディスクリプタを閉じています.ここまでの処理が円滑に進むと保証されていればいいのですが,そうでなければ close() に行きつかず,ファイルディスクリプタが閉じられないまま異常終了するおそれがあります.そこで,ファイルを開くのに使う組み込み関数 open() と同様に,with 文を用いた方法をとりたくなります.この仕組みは,乱暴に言えば,open() の返り値にコンテキストマネージャとしての性質があるために実現しています.しかし,PdfFileMerger の場合にはその仕組みが備わっていません.そこで,上記のように @contextlib.contextmanager デコレータを用い,open() 同様のコンテキストマネージャのファクトリ関数 get_merger() を自分で書いています.これを使って,お馴染みの with 文を用いた idiom で,close() 忘れの心配なしに安全にファイルを扱えます.

参考にした本

『退屈なことは Python にやらせよう』において,同様の問題設定で PyPDF2 が紹介されていたため,これを参考にしました.長い間ただ本棚に鎮座していましたが,久しぶりに活用できました.