feat: 固定フォルダ構成・ロック・ログ統合のmain()に置き換え

旧CLI引数型(argparse)を廃止し、config.txt駆動の固定フォルダ運用に一本化。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kenichiro NOGI 2026-08-02 10:14:37 +09:00
parent 0a0bb057e2
commit 1d283eb1c6

View File

@ -1,24 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
テンプレート(赤枠=氏名, 青枠=日付)を基準に対象フォルダ内のスキャンPDFから テンプレート(赤枠=氏名, 青枠=日付)を基準に"スキャン"フォルダ内のPDFから
氏名日付をOCR抽出し"output/氏名/YYYYMMDD_氏名.pdf" にリネーム移動する 氏名日付をOCR抽出し"アウトプット/氏名/氏名_YYYYMMDD.pdf" にコピー
元ファイルは"成功"または"失敗"フォルダへ移動する
使い方: 起動は OCR仕分け実行.bat から行う想定config.txt の固定フォルダ構成に依存
python extract_and_rename.py \
--template template.png \
--input ./inbox \
--output ./output \
--failed ./failed \
--margin 0.10
必要ライブラリ: opencv-python, pdf2image, pytesseract, pillow, numpy 必要ライブラリ: opencv-python, pdf2image, pytesseract, pillow, numpy
必要な外部ツール: tesseract-ocr, tesseract-ocr-jpn, poppler-utils(pdftoppm) 必要な外部ツール: tesseract-ocr, tesseract-ocr-jpn, poppler-utils(pdftoppm)
""" """
import argparse import importlib
import re import re
import shutil import time
import sys import traceback
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@ -27,11 +22,13 @@ from pdf2image import convert_from_path
from PIL import Image from PIL import Image
from box_detector import detect_template_fields, to_ratio_box from box_detector import detect_template_fields, to_ratio_box
from config_loader import load_config
from file_ops import copy_with_unique_name, move_with_unique_name, wait_until_stable
from lock_manager import LockAcquisitionError, acquire_lock, release_lock
from logger import append_log
DPI = 300 # PDF→画像変換の解像度。テンプレートも同じDPIで作成/スキャンしてください DPI = 300 # PDF→画像変換の解像度。テンプレートも同じDPIで作成/スキャンしてください
import importlib
TEMPLATE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp"} TEMPLATE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp"}
@ -69,13 +66,6 @@ def check_external_tools(tesseract_exe: Path, poppler_exe: Path) -> list[str]:
return missing return missing
import time
import traceback
from file_ops import copy_with_unique_name, move_with_unique_name, wait_until_stable
from logger import append_log
def key_pressed() -> bool: def key_pressed() -> bool:
import msvcrt import msvcrt
@ -231,77 +221,84 @@ def run_queue(
append_log(log_dir, "成功", pdf_path.name, name, date, "") append_log(log_dir, "成功", pdf_path.name, name, date, "")
def safe_move(src: Path, dest_dir: Path, filename: str) -> Path: HOME_DIR = Path(__file__).resolve().parent.parent
dest_dir.mkdir(parents=True, exist_ok=True) RUNTIME_FOLDER_KEYS = (
dest_path = dest_dir / filename "テンプレートフォルダ",
# 同名ファイルが既にある場合は連番を付けて衝突を回避 "スキャンフォルダ",
counter = 1 "アウトプットフォルダ",
while dest_path.exists(): "成功フォルダ",
stem = Path(filename).stem "失敗フォルダ",
suffix = Path(filename).suffix "ログフォルダ",
dest_path = dest_dir / f"{stem}_{counter}{suffix}" )
counter += 1
shutil.move(str(src), str(dest_path))
return dest_path
def main(): def main() -> None:
parser = argparse.ArgumentParser(description="テンプレート枠を基準にPDFから氏名・日付を抽出してリネーム・移動する") config_path = HOME_DIR / "config.txt"
parser.add_argument("--template", required=True, help="赤枠(氏名)・青枠(日付)付きテンプレート画像") try:
parser.add_argument("--input", required=True, help="処理対象PDFが入ったフォルダ") config = load_config(config_path)
parser.add_argument("--output", required=True, help="成功時の出力先ルートフォルダ(氏名ごとにサブフォルダ作成)") except Exception as e:
parser.add_argument("--failed", required=True, help="抽出失敗時の退避先フォルダ") print(f"設定エラー: {e}")
parser.add_argument("--margin", type=float, default=0.10, help="枠の許容誤差率(デフォルト 0.10 = 10%%") input("何かキーを押すと終了します...")
args = parser.parse_args() return
template_path = Path(args.template) folders = {key: HOME_DIR / config[key] for key in RUNTIME_FOLDER_KEYS}
input_dir = Path(args.input) for folder in folders.values():
output_dir = Path(args.output) folder.mkdir(parents=True, exist_ok=True)
failed_dir = Path(args.failed)
if not template_path.exists(): missing_modules = check_dependencies()
sys.exit(f"テンプレート画像が見つかりません: {template_path}") if missing_modules:
if not input_dir.exists(): print(f"配布パッケージが壊れています。不足モジュール: {', '.join(missing_modules)}")
sys.exit(f"入力フォルダが見つかりません: {input_dir}") input("何かキーを押すと終了します...")
return
script_dir = HOME_DIR / config["スクリプトフォルダ"]
tesseract_exe = script_dir / "tools" / "tesseract" / "tesseract.exe"
poppler_exe = script_dir / "tools" / "poppler" / "pdftoppm.exe"
missing_tools = check_external_tools(tesseract_exe, poppler_exe)
if missing_tools:
print(f"配布パッケージが壊れています。不足ファイル: {', '.join(missing_tools)}")
input("何かキーを押すと終了します...")
return
lock_path = HOME_DIR / ".lock"
try:
acquire_lock(lock_path)
except LockAcquisitionError as e:
print(str(e))
input("何かキーを押すと終了します...")
return
try:
try:
template_path = select_template_file(folders["テンプレートフォルダ"])
except FileNotFoundError as e:
print(str(e))
return
print(f"テンプレート採用: {template_path.name}")
boxes = load_field_boxes(str(template_path)) boxes = load_field_boxes(str(template_path))
print(f"検出フィールド: {[b.label for b in boxes]}") print(f"検出フィールド: {[b.label for b in boxes]}")
pdf_files = sorted(input_dir.glob("*.pdf")) margin = float(config["margin"])
if not pdf_files: stable_wait_sec = float(config["ファイル安定待ち秒"])
print("対象PDFが見つかりませんでした。") stable_retries = int(config["ファイル安定待ちリトライ回数"])
return
results = [] run_queue(
for pdf_path in pdf_files: folders["スキャンフォルダ"],
print(f"処理中: {pdf_path.name}") folders["アウトプットフォルダ"],
try: folders["成功フォルダ"],
name, date = process_pdf(pdf_path, boxes, args.margin) folders["失敗フォルダ"],
except Exception as e: folders["ログフォルダ"],
print(f" エラー: {e}") boxes,
safe_move(pdf_path, failed_dir, pdf_path.name) margin,
results.append((pdf_path.name, None, None, "error", str(e))) stable_wait_sec,
continue stable_retries,
)
finally:
release_lock(lock_path)
if not name or not date: print("処理完了。")
print(f" 抽出不十分(氏名={name!r}, 日付={date!r})→ failed へ退避") input("何かキーを押すと終了します...")
safe_move(pdf_path, failed_dir, pdf_path.name)
results.append((pdf_path.name, name, date, "failed", ""))
continue
new_filename = f"{date}_{name}.pdf"
dest_dir = output_dir / name
moved_path = safe_move(pdf_path, dest_dir, new_filename)
print(f"{moved_path}")
results.append((pdf_path.name, name, date, "success", str(moved_path)))
# サマリー出力
print("\n=== 処理結果サマリー ===")
for orig, name, date, status, note in results:
print(f"{status:8s} | {orig:30s} | 氏名={name} 日付={date} | {note}")
success_count = sum(1 for r in results if r[3] == "success")
print(f"\n合計 {len(results)} 件中 {success_count} 件成功")
if __name__ == "__main__": if __name__ == "__main__":