たくさんのPDFを正規表現で一括検索する方法です。
プログラムからのPDF検索で一番難しいのは、PDFのテキストにアクセスする部分です。簡単に読み込めるPDFもあれば、そうでないPDFもあります。この記事では、そのようなPDFも含めて、PDFのテキストにアクセスする方法を主に紹介します。
その前に、Adobe Rearder(アドビリーダー)でも「一括検索」や「あいまいな検索」が可能です。正規表現こそ使えませんが、「ちょっと、このフォルダのPDFを調べたい」といった時ならば、これが最速です。
「Shift+Ctrl+F」で「高度な検索」を呼び出します。「検索する場所:」が「現在の文書」になっていますので、そこを調べたいフォルダに変更すれば、ヒットしたPDFを列挙してくれます。とても簡単です。
以下、PythonからPDFを検索する方法です。
PDFをテキストに変換して検索する
検索方法ですが、ピーディーエフ・トゥ・テキスト(pdftotext.exe)でテキストに変換して、パイソン(Python)の正規表現モジュールで検索します。pdftotext.exeも、パイソンから呼び出します。テキストファイルにしてしまえば、あとはどのようにでも検索できます。
ところが、エディネット(EDINET)のPDFのように、読めるけど変換できないものがあります。これが問題です。対応方法ですが、このようなPDFでもGoogle翻訳に渡せば何事もなく翻訳してくれるように、PDFのフラグを変更することでテキスト変換ができるようになります。
それは、キュー・ピーディーエフ(qpdf.exe)で変更できます。これもパイソンから呼び出します。まとめると、PDFファイル ⇒ フラグ変更して保存 ⇒ テキスト変換して保存 ⇒ 正規表現で検索、という流れになります。
検索結果はリストにでも追加していって、CSVなどに保存する感じです。BOM付きUTF-8 (utf-8-sig) で保存しておけば、文字化けせずにExcelで開けて便利です。
以下は、「qpdf.exe」と「pdftotext.exe」の取得・インストール方法と、パイソンからの呼び出し方です。
qpdf.exe を取得
これは「QPDF」というプロジェクト名で公開されています。ギットハブから取得します。
QPDF ダウンロードページ (GitHub)
https://github.com/qpdf/qpdf/releases
使うのは、Windows用の「qpdf-8.1.0-bin-msvc64.zip」です。
(バージョンはそのときの最新版でいいと思います)
この中に「qpdf.exe」が含まれています。
pdftotext.exe を取得 & 日本語設定
取得
こちらは「エックス・ピーディーエフ・ツールズ (Xpdf tools)」というソフトに含まれています。
Xpdf tools は、Windows用の「Windows 32/64-bit」を使いました。
以下のページの「Download the Xpdf command line tools:」から取得します。
Xpdf tools ダウンロードページ
https://www.xpdfreader.com/download.html
あわせて、日本語対応のための言語サポートパッケージも取得します。
同じページにある「Download language support packages for Xpdf:」から取得します。
言語サポートパッケージは、「Japanese」を使いました。
日本語設定
Xpdf toolsと言語サポートパッケージを、以下のようなフォルダ構成で解凍します。インストーラーなどは無いので、好きなフォルダに配置してOKです。バージョン番号などを削って短くしても大丈夫です。
Xpdf tools フォルダ(例)
D:\project\tool\xpdf-tools-win-4.00
言語サポートパッケージ フォルダ(例)
D:\project\tool\xpdf-tools-win-4.00\japanese
日本語設定の方法は、言語サポートパッケージ(xpdf-japanese.tar.gz)の中の「README」に載っています。上記のフォルダ構成に合わせて要約すると、手順は以下の通りです。
(1. と 2. のリネームはしなくても動くのですが、一応、READMEの内容に合わせてリネームします。重要なのは 3. の中身です。)
- 「xpdf-japanese」を「japanese」にリネーム。
- 「add-to-xpdfrc」を「
D:\project\tool\xpdf-tools-win-4.00
」にコピーして、「xpdfrc」にリネーム。 - 「xpdfrc」の中身を以下のように変更。
変更前
#----- begin Japanese support package (2011-sep-02)
cidToUnicode Adobe-Japan1 /usr/local/share/xpdf/japanese/Adobe-Japan1.cidToUnicode
unicodeMap ISO-2022-JP /usr/local/share/xpdf/japanese/ISO-2022-JP.unicodeMap
unicodeMap EUC-JP /usr/local/share/xpdf/japanese/EUC-JP.unicodeMap
unicodeMap Shift-JIS /usr/local/share/xpdf/japanese/Shift-JIS.unicodeMap
cMapDir Adobe-Japan1 /usr/local/share/xpdf/japanese/CMap
toUnicodeDir /usr/local/share/xpdf/japanese/CMap
#fontFileCC Adobe-Japan1 /usr/..../NotoSansCJKjp-Regular.otf
#----- end Japanese support package
変更後
#----- begin Japanese support package (2011-sep-02)
cidToUnicode Adobe-Japan1 "D:\project\tool\xpdf-tools-win-4.00\japanese\Adobe-Japan1.cidToUnicode"
unicodeMap ISO-2022-JP "D:\project\tool\xpdf-tools-win-4.00\japanese\ISO-2022-JP.unicodeMap"
unicodeMap EUC-JP "D:\project\tool\xpdf-tools-win-4.00\japanese\EUC-JP.unicodeMap"
unicodeMap Shift-JIS "D:\project\tool\xpdf-tools-win-4.00\japanese\Shift-JIS.unicodeMap"
cMapDir Adobe-Japan1 "D:\project\tool\xpdf-tools-win-4.00\japanese\CMap"
toUnicodeDir "D:\project\tool\xpdf-tools-win-4.00\japanese\CMap"
#fontFileCC Adobe-Japan1 /usr/..../NotoSansCJKjp-Regular.otf
#----- end Japanese support package
シャープ「#」から始まる行は削っても大丈夫です。begin、fontFileCC、endの行がありますが、全て削除しても大丈夫です。
ファイルパスとフォルダパスは、全て絶対パスで指定します。絶対パスさえ合っていれば、途中のフォルダ名などは何でもOKでした。そして、相対パスは使えないようです。色々試したのですが、うまく行かなかったです。
この「xpdfrc」へのファイルパスは、パイソンから呼び出すときのコマンドライン文字列に追加します。
Pythonからqpdfとpdftotextを呼び出す
モジュール群のインポート
exeファイルを使用するので、subprocess関連をインポートします。
import os
from subprocess import Popen
from subprocess import PIPE
from subprocess import TimeoutExpired
from traceback import format_exc
フラグ変更関数 (qpdf.exe)
qpdf のコマンドライン文字列を生成して、実行する関数です。オプションの説明はQPDFに同梱されているマニュアルに載っています。
qpdfの使い方です。
フラグ変更にあたって、パスワードは特に必要ありません。なので、空の文字列(""
)を指定しています。
def change_flag(qpdf_exe, in_file, out_file, timeout=30):
"""PDFのフラグを変更して保存"""
# コマンドライン文字列を生成
cmd = '"{exe}" {password} {decrypt} "{i}" "{o}"'.format(
exe=qpdf_exe,
password='--password=""',
i=in_file,
decrypt='--decrypt',
o=out_file,
)
# 子プロセス実行
with Popen(
cmd,
stdin=None, stdout=PIPE, stderr=PIPE,
universal_newlines=True,
) as p:
try:
# 子プロセスの終了を待つ
(outs, errs) = p.communicate(timeout=timeout)
except TimeoutExpired:
# 子プロセスを終了
p.kill()
# 通信を再試行
(outs, errs) = p.communicate(timeout=timeout)
else:
pass
return (cmd, p.returncode, outs, errs)
コマンドライン文字列の例です。
"D:\\project\\tool\\qpdf-8.1.0\\bin\\qpdf.exe" --password="" --decrypt "D:\\project\\data1_pdf\\S100DA2Y.pdf" "D:\\project\\data2_pdf\\S100DA2Y.pdf"
あと、コードの細かいところの説明です。
サブプロセス・プロセスオープン(subprocess.Popen)の使い方は、Pythonの公式マニュアルに載っていました。「Popen」の「P」は、たぶんプロセスのPだと思いますが、よくわかりません。
標準入力(stdin)は使わないので、Noneを指定しています。そのほかは、何かテキストが表示される場合がありますので、PIPEを指定して取得できるようにしています。
「universal_newlines」に「True」を設定しておくことで、標準出力(stdout)と標準エラー出力(stderr)の内容をテキスト文字列で取得できます。Falseだとバイナリが返ってきます。
Python 3.2 からは「with文」が使えるようになったみたいなので、使っています。
qpdfは一瞬で完了するので、p.communicate() でのタイムアウトはまず発生しません。とりあえず30秒を指定しています。
テキスト変換関数 (pdftotext.exe)
pdftotext のコマンドライン文字列を生成して、実行する関数です。設定ファイル(xpdfrc)へのファイルパスも指定します。オプションの説明は xpdf-tools に同梱されているマニュアル(pdftotext.txt)に載っています。
抽出テキストを保存するときのエンコーディングですが、「UTF-8」や「Shift-JIS」を指定します。Pythonのエンコーディング指定と違って、utf-8 や shift-jis だとエラーになります。通常は全て「UTF-8」で良いと思います。
ノー・ページ・ブレーク「-nopgbrk」は、ページ区切りを挿入しない、というオプションです。使わないテキストを追加されても困るので、このオプションでオフにしておきます。
def extract_text(
pdftotext_exe, xpdfrc_file,in_file, out_file, out_encoding,
timeout=30):
"""PDFからテキストを抽出して保存"""
# コマンドライン文字列を生成
cmd = '"{exe}" -cfg "{xpdfrc}" -enc {enc} -nopgbrk "{i}" "{o}"'.format(
exe=pdftotext_exe,
xpdfrc=xpdfrc_file,
enc=out_encoding,
i=in_file,
o=out_file,
)
# 子プロセス実行
with Popen(
cmd,
stdin=None, stdout=PIPE, stderr=PIPE,
universal_newlines=True,
) as p:
try:
# 子プロセスの終了を待つ
(outs, errs) = p.communicate(timeout=timeout)
except TimeoutExpired:
# 子プロセスを終了
p.kill()
# 通信を再試行
(outs, errs) = p.communicate(timeout=timeout)
return (cmd, p.returncode, outs, errs)
コマンドライン文字列の例です。
"D:\\project\\tool\\xpdf-tools-win-4.00\\bin64\\pdftotext.exe" -cfg "D:\\project\\tool\\xpdf-tools-win-4.00\\xpdfrc" -enc UTF-8 -nopgbrk "D:\\project\\data2_pdf\\S100DA2Y.pdf" "D:\\project\\data3_text\\S100DA2Y.txt"
あと、pdftotext.exe のエラー集です。pdfrcの編集を終えて、テキスト抽出が出来るようになるまでがちょっと大変でした。
1つ目です。
Syntax Error (824519): No font in show/space
Syntax Error: Unknown font tag 'F0'
Syntax Error (824535): No font in show
Syntax Error: Unknown character collection 'Adobe-Japan1'
Syntax Error: Couldn't find 'UniJIS-UCS2-H' CMap file for 'Adobe-Japan1' collection
Syntax Error: Unknown CMap 'UniJIS-UCS2-H' for character collection 'Adobe-Japan1'
Syntax Error: Failed to parse font object for 'MS-Mincho'
Syntax Error: Unknown character collection 'Adobe-Japan1'
Syntax Error: Couldn't find 'UniJIS-UCS2-H' CMap file for 'Adobe-Japan1' collection
Syntax Error: Unknown CMap 'UniJIS-UCS2-H' for character collection 'Adobe-Japan1'
Syntax Error: Failed to parse font object for 'HeiseiMin-W3-UniJIS-UCS2-H'
これは、以下の場合に出てきました。
- xpdfrc を設置していなかった。
- xpdfrcへのファイルパスが正しくなかった。
- xpdfrcの中身を正しく書き換えていなかった。
「pdftotext.exe」に「xpdfrc」のファイルパスを「渡しているか?」または「渡せているか?」を見るといいかもしれません。
あと、中身については、絶対パスで書いていることと、パスをダブルクオーテーション「”」で囲っていることを見るといいかもしれません。
2つ目です。
Syntax Error: Couldn't find unicodeMap file for the 'utf-8' encoding
Config Error: Couldn't get text encoding
これは、コマンドライン文字列の中の「-enc」オプションが間違ってる時に出てきました。
「-enc」に続く文字列は、大文字・小文字が決まっています。「UTF-8」や「Shift-JIS」が有効です。小文字で「utf-8」と指定しても「pdftotext.exe」が受け付けられず、エラーとなります。
問題が解消されれば、pdftotextもほぼ一瞬でテキストを出力してくれます。
メイン関数
実際に検索をするところです。PDFフォルダのファイルを列挙して、テキスト変換と正規表現検索を順に実行していく感じです。
検索部分は省略しています。実際には、re.findall() や re.search()の戻り値からヒットした文字列を取得して、ファイル名などと共にCSVに出力する感じになるかと思います。
フォルダパス・ファイルパスの設定は、ご自身の管理されているシステムに合わせて読み替えてください。
def main():
"""テスト"""
# コマンドラインツールのファイルパス
qpdf_exe = r'D:\project\tool\qpdf-8.1.0\bin\qpdf.exe'
pdftotext_exe = r'D:\project\tool\xpdf-tools-win-4.00\bin64\pdftotext.exe'
xpdfrc_file = r"D:\project\tool\xpdf-tools-win-4.00\xpdfrc"
# 子プロセスのタイムアウト秒数
timeout = 30
# 抽出したテキストを保存する時のエンコーディング
pdftotext_encoding = 'UTF-8' # UTFは大文字でUTF
# pdftotext_encoding = 'Shift-JIS'
# 検索対象のPDFフォルダ
data1_pdf_dir = r'D:\project\data1_pdf'
# qpdf 保存フォルダ
data2_pdf_dir = r'D:\project\data2_pdf'
if not os.path.isdir(data2_pdf_dir):
os.mkdir(data2_pdf_dir)
# pdftotext 保存フォルダ
data3_text_dir = r'D:\project\data3_text'
if not os.path.isdir(data3_text_dir):
os.mkdir(data3_text_dir)
# フォルダ内のPDFを列挙
for name in os.listdir(data1_pdf_dir):
# PDFでなければスキップ
if os.path.splitext(name)[1].lower() != '.pdf':
continue
# qpdf 入力・出力ファイルパス設定
in_pdf = os.path.join(data1_pdf_dir, name)
out_pdf = os.path.join(data2_pdf_dir, name)
if not os.path.isfile(out_pdf):
# PDFのフラグを変更して保存
(cmd, returncode, outs, errs) = change_flag(
qpdf_exe, in_pdf, out_pdf, timeout)
if returncode != 0:
print('%s\n%s\n%s\n%s' % (
cmd, returncode, outs, errs))
elif errs:
print('%s\n%s\n%s\n%s' % (
cmd, returncode, outs, errs))
# pdftotext 出力ファイルパス設定
out_txt = os.path.join(
data3_text_dir,
'%s.txt' % os.path.splitext(name)[0])
if not os.path.isfile(out_txt):
# PDFからテキストを抽出して保存
(cmd, returncode, outs, errs) = extract_text(
pdftotext_exe, xpdfrc_file, out_pdf, out_txt,
pdftotext_encoding, timeout)
if returncode != 0:
print('%s\n%s\n%s\n%s' % (
cmd, returncode, outs, errs))
elif errs:
print('%s\n%s\n%s\n%s' % (
cmd, returncode, outs, errs))
# あとは、保存したテキストファイルを開いて、
# Pythonの正規表現モジュールで検索します。
return
最新の開示情報を一括チェックするときに便利
このPDF検索システムですが、もともとは最新の開示情報をふるいにかけたり、ある企業の全開示情報の変遷や情報の出所を調べるために作りました。
そのあたりの内容は、以下の記事にあります。
PDFを正規表現で一括検索するシステムを作った経験談(EDINET PDF対応)
あらかじめ好印象の文字列などを正規表現にしておくことで、増収増益が踊る開示をその日のうちに見つけることができます。まあ、しかしながら、それが直ちに何かの成果に結びつくという単純なものでもないとは思いますが、開示情報の検索や調べ物がとても楽になりました。
標準入力は使えないのか?
標準入力(stdin)が使えれば、ファイルをHDDやSSDに保存する必要がありません。
なので、もっと高速で省スペースなテキスト検索ができると思いました。
そこでヘルプファイルを調べたのですが、「pdftotext.exe」には標準入力からPDFを受け取るオプションがありませんでした。
なので、標準入力は使えないということになります。
テキストの抽出結果を「標準出力に出す」ことはできるんですが、標準入力からPDFを受け取ることはできないようでした。
もし、標準入力からPDFを受け取れたなら、「qpdf.exe」が標準出力に書き込んだPDFデータを「pdftotext.exe」の標準入力で受け取ってテキストにする、みたいなことが出来たんですけれども。
「どうしてだろう?」とPDF関係のフォーラムをいろいろ調べた結果、「あえて標準入力から読み込む機能を作っていない」とのことでした。
出来なくもないけど、PDF全体をメモリに読み込む必要があるので、巨大なPDFが扱えなくなる、というのが主な理由のようでした。
PDFは構造上、データをシークしながら読み込む必要があって、それを標準入力で実現するために、PDF全体をメモリに読み込む必要があるとのことでした。
以上です。