本日から!PDFの一括処理で時間短縮

社内ドキュメントの扱いに費やす時間は小さくありません。統計ではナレッジワーカーが情報の検索・整形に費やす時間が業務全体の約19%に達するという報告があり¹²、請求書や契約書などPDFの占める比率は年々増えています⁴。運用現場の観察でも、PDFの前処理が後段のワークフローを塞ぎやすく、待ち時間が日次のSLA(サービス品質合意)を侵食する傾向が見られます。そこで重要になるのが、一括処理(バッチ処理)を前提にした設計と、軽量な自動化(PDF処理の自動化)の導入です。ポイントは、今日から動かせる最小構成を用意し、ボトルネックを計測しながら拡張していくことに尽きます。
なぜ「いま」PDFを一括処理に載せるのか
単発の手作業で回っているうちは見過ごされがちですが、日次で数百から数千件のPDFが発生する現場では、個別処理は即座に限界に達します。エンドユーザーの価値はデータ化された内容にあり、PDF自体は通過点に過ぎません。つまり人手の介在をできる限り前段で排除し、テキスト抽出(PDFから文字データを取り出す処理)、分割・結合、最適化(サイズや表示速度の調整)、画像化、OCR(光学文字認識)、メタデータ付与(作成者や日付などの情報を持たせる)といった作業をパイプライン化するだけで、待ち時間とヒューマンエラーの双方が目に見えて減少します。実務では、暗号化やフォント埋め込みの不整合、巨大ページやスキャン品質のばらつきなど、例外が必ず混ざります。だからこそ一括処理の設計では、冪等性(同じ入力に対して同じ結果が出る性質)、再実行可能性、そして失敗を前提にした分岐と隔離が最初の要件になります。
最小構成で始める設計のスケッチ
保守可能性を犠牲にしない最小構成はシンプルです。着地点は「監視対象のストレージに置かれたPDFを、検証・処理・成果物保存・指標記録まで自走させる」こと。ストレージイベントのフック(例:新規アップロードの通知)、キューによるバックプレッシャー(過負荷を抑える仕組み)、処理ワーカーのスケール、そして結果のハッシュとマニフェストで冪等性を担保します。テキスト主体のファイルには抽出、スキャン主体には画像化とOCR、混在にはハイブリッド抽出を適用し、分岐の判断は先頭数ページの軽量判定で済ませます。ここまでの構成で、現場の大半は「前段の待ち」を本日から解消できます。
テキスト抽出の最小コード(Python/PyMuPDF)
import sys, json, time
import fitz # PyMuPDF
from pathlib import Path
def extract_text(path: Path) -> dict:
start = time.perf_counter()
try:
with fitz.open(path) as doc:
meta = doc.metadata or {}
text = []
for page in doc:
text.append(page.get_text("text"))
pages = doc.page_count
return {"ok": True, "seconds": round(time.perf_counter() - start, 3),
"pages": pages, "meta": meta, "text": "\n".join(text)}
except Exception as e:
return {"ok": False, "error": str(e)}
if __name__ == "__main__":
p = Path(sys.argv[1])
print(json.dumps(extract_text(p), ensure_ascii=False))
PyMuPDFはC実装に近く、テキストPDFであれば数十〜百ページ/秒/スレッド程度(環境に依存)の実効が出ます。障害は例外で返し、上位のワーカーから再試行や隔離に回すのが安全です。暗号化PDFや壊れたファイルは投入前に軽く検証し、処理対象から自動で外すとフローが安定します。
分割・結合・最適化をワンストップで
実務では結合と分割が頻出します。契約書の追補をマージしたい、請求書を明細ごとにスライスしたい、あるいは配信のためにファイルを線形化して早く開かせたい(Fast Web Viewに最適化したい)、といった要求です。ライセンスや運用負荷を抑え、OSS(オープンソースソフトウェア)を軸に組むと良いでしょう。
結合とページ抽出(Python/pypdf)
from pypdf import PdfReader, PdfWriter
def merge(out_path, *inputs):
w = PdfWriter()
for i in inputs:
r = PdfReader(i)
[w.add_page(p) for p in r.pages]
with open(out_path, "wb") as f:
w.write(f)
def slice_pages(src, out_path, start, end):
r = PdfReader(src)
w = PdfWriter()
for i in range(start, end):
w.add_page(r.pages[i])
with open(out_path, "wb") as f:
w.write(f)
表示の初期体感を改善したい場合、線形化(Fast Web View)を入れると効果が出ます。OSSのqpdfで後処理するのが軽量です。
線形化と圧縮(qpdf/Ghostscriptの呼び出し)
# 線形化(Fast Web View)
qpdf --linearize input.pdf output_linearized.pdf
# 画像のダウンサンプリングと圧縮(印刷での軽量化)
gs -sDEVICE=pdfwrite -dPDFSETTINGS=/printer -o out.pdf input.pdf
テキストベースでは線形化、スキャンベースでは解像度の適正化が効きます。業務要件ごとにプリセットを用意し、ファイルサイズの分布を可視化して継続的に閾値を調整すると、配信用の帯域と開封速度の折り合いが取りやすくなります。
スループット最適化とベンチマークの進め方
バッチのボトルネックはI/O、CPU、外部コマンドの待ちの三つに集約されます。I/Oは非同期で重ね、CPUはページ単位やファイル単位で並列化し、外部コマンドはワーカープールで数を絞ります。温め(ウォームアップ)を入れてJITやページキャッシュの恩恵を得るだけでも、短命ジョブの分散では効きます。計測は処理段ごとに時刻を打ち、PDFのページ数、サイズ、スキャン率をタグに持たせると改善余地が浮かび上がります。
ページ並列での抽出(Python/multiprocessing)
import fitz, time
from multiprocessing import Pool
def page_text(arg):
path, idx = arg
with fitz.open(path) as d:
return d.load_page(idx).get_text("text")
def extract_parallel(path, workers=4):
start = time.perf_counter()
with fitz.open(path) as d, Pool(workers) as pool:
texts = pool.map(page_text, [(path, i) for i in range(d.page_count)])
return {"pps": len(texts) / (time.perf_counter() - start)}
テキスト主体の社内資料であれば、4ワーカーで60–150 pages/sec程度が現実的なレンジになります(入出力とCPUに依存)。CPUよりI/Oが詰まるときは、ファイルの事前配置やNVMeローカル一時領域への展開が効きます。
OCRのハイブリッド処理(Python/pytesseract)
import fitz, pytesseract
from PIL import Image
def ocr_first_page(path):
with fitz.open(path) as d:
pix = d[0].get_pixmap(dpi=300)
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
return pytesseract.image_to_string(img, lang="jpn+eng")
全ページOCRは高コストです。先頭数ページでスキャン率を推定し、必要なときだけOCRを混ぜるだけで、1桁のコスト削減につながるケースが珍しくありません³。品質指標はリコール重視で設計し、再走可能なリストに積む運用が安全です。
Node.jsでのワーカープールと線形化
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const { spawn } = require('child_process');
if (!isMainThread) {
const run = (inPath, outPath) => new Promise((res, rej) => {
const p = spawn('qpdf', ['--linearize', inPath, outPath]);
p.on('close', c => c === 0 ? res() : rej(new Error('qpdf exit ' + c)));
});
run(workerData.in, workerData.out).then(() => parentPort.postMessage('ok')).catch(e => parentPort.postMessage(e.message));
}
ワーカープールのサイズはCPUコア数より少なめに抑え、I/Oの重なりを優先するとスループットが安定します。外部コマンドは終了コードと標準エラーを必ず収集し、再試行可能な失敗と致命的エラーを分けて扱います。
Goでの軽量最適化(pdfcpu)
package main
import (
"log"
"github.com/pdfcpu/pdfcpu/pkg/api"
)
func main(){
in, out := "in.pdf", "out.pdf"
if err := api.OptimizeFile(in, out, nil); err != nil { log.Fatal(err) }
}
単機能のCLIをマイクロサービスとして切り出すと、ジョブキューと組み合わせたときの障害分離が容易になります。Docker化してリソースを固定すれば、SLAの議論がしやすくなります。
セキュリティ、運用、そしてROI
取り扱うPDFには個人情報や機密が含まれます。暗号化ファイルのスキップや隔離、平文への展開禁止、保存時のサーバーサイド暗号化、処理ノードの一時領域の自動消去、そしてアクセス監査は早い段階で仕込むべきです。検出から隔離までの導線を短くし、監査ログにはハッシュ、サイズ、ページ数、ハンドラーのバージョンを残すと、後追いの調査が容易になります。
単純なマスキング(PyMuPDFでの矩形レダクション)
import fitz
import re
def redact_phone(src, out):
with fitz.open(src) as d:
for p in d:
text = p.get_text("text")
for m in re.finditer(r"\b\d{2,4}-\d{2,4}-\d{3,4}\b", text):
for inst in p.search_for(m.group()):
p.add_redact_annot(inst, fill=(0,0,0))
p.apply_redactions()
d.save(out)
正規表現ベースの単純なレダクションでも、流出リスクを大きく下げられます。高精度が必要な領域は別途人手のスポット審査に回し、閾値で仕分けるのがコスト効率の良い落としどころです。
効果の見積もりは単純で、1件あたり45秒の手作業を3秒の自動処理に置き換えるだけで、日次2,000件の現場なら約23.3時間/日の削減という目安になります。月間20営業日で約466時間、時給3,000円換算なら約140万円/月の改善です。加えて、SLA遵守とヒューマンエラーの低減、属人化の解消が定性的な効用として積み上がります。初期導入は既存のストレージとキューを活用し、処理ワーカーをコンテナで数台用意するだけで始められます。効果が見えた段階で、OCRや検索インデックス、レダクションの高度化に投資していくのが筋の良い進め方です。
まとめ:今日動かし、明日から育てる
PDFの一括処理は大掛かりな刷新を必要としません。監視対象のストレージ、軽量なキュー、数本のワーカー、そして計測の仕組みさえ用意すれば、今日から待ち時間を削り始められます。まずはテキスト抽出と結合・分割、線形化の三点に絞り、計測で見えたボトルネックに対して並列化と一時領域の工夫を加えてください。スキャン主体が多い場合はハイブリッドOCRを後段に挿し、機微情報には簡易レダクションを併用すると、リスクとコストのバランスが取れます。
次にどこを自動化すると最も時間が戻ってくるのか、あなたの現場の数字で一度試算してみませんか。手持ちのインフラで最小構成を立ち上げ、明日のジョブからバッチに載せ替える。それが、ドキュメントを価値に変えるいちばん短い道筋です。
参考文献
- McKinsey & Company. Rethinking knowledge work: a strategic approach. https://www.mckinsey.com/capabilities/people-and-organizational-performance/our-insights/rethinking-knowledge-work-a-strategic-approach.#:~:text=The%20problems%20of%20free%20access,typical%20businesses%20is%20posted%20to
- KMWorld. According to Interact Source… https://www.kmworld.com/Articles/ReadArticle.aspx?ArticleID=135756&pageNum=2#:~:text=According%20to%20Interact%20Source%2C%20time%E2%80%94the,to%20do%20their%20jobs%20effectively
- Alteryx Press Release (2018-01-29). Data professionals waste 50 percent of their time on unsuccessful or repeated data activities. https://www.alteryx.com/about-us/newsroom/press-release/2018-01-29-data-professionals-waste-50-percent-time-unsuccessful-or-repeated-data#:~:text=,unsuccessful%20activities%20or%20repeating%20efforts
- Forbes Technology Council (2019-12-17). Reality Check: Still Spending More Time Gathering Instead Of Analyzing. https://www.forbes.com/councils/forbestechcouncil/2019/12/17/reality-check-still-spending-more-time-gathering-instead-of-analyzing/#:~:text=decade%20after%20the%20IDC%20report%2C,searching