Actions
새기능 #5794
완료됨[공유저작물 동의서] 스캔 PDF OCR 파일명 자동변경기 (PDF_rename.exe)
설명
개요¶
스캐너에서 20260710121527.pdf 처럼 스캔 시각으로만 저장되는 「공유저작물 이용허락 동의서」 스캔본을, 첫 페이지를 OCR로 읽어 YYYYMMDD_성명.pdf (동의서 작성일자 + 동의자 성명) 형식으로 자동 리네임하는 Windows exe 도구를 제작했다.
- 실행파일:
PDF_rename.exe(PyInstaller onedir, 폴더째 배포PDF_rename_배포용.zip) - 소스:
rename_pdf.py - 사용법: exe를 PDF가 들어있는 폴더에 넣고 더블클릭 → 그 폴더의 모든 PDF를 처리
- 결과 로그: 같은 폴더에
rename_log.txt누적 기록
===== 2026-07-10 16:11:05 =====
[완료] 20260710111618.pdf -> 20131112_이만수.pdf
[완료] 20260710111703.pdf -> 20131129_이미숙.pdf
처리 흐름¶
- 대상 선별 — 실행 폴더의
*.pdf중 이미^\d{8}_.+형식인 파일은[건너뜀-이미변경됨]처리. 재실행해도 안전(멱등). - 첫 페이지 래스터화 — PyMuPDF(fitz)로 1페이지만 120dpi 픽스맵 → numpy RGB 배열. (RGBA/Gray 자동 보정)
- 밴드 크롭(1차, 빠른 경로) — 동의서 양식은 상단에 성명/생년월일 표, 하단에 작성일자가 고정 위치. 페이지 세로 30%~85% 구간만 잘라 OCR → 인식 영역 축소로 속도 확보.
- OCR — RapidOCR(onnxruntime) + 한글 PP-OCRv5 mobile 모델.
- 날짜 추출 — OCR 텍스트를 한 줄로 이어붙여
(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})매칭. 본문에도 연도가 나올 수 있어 마지막 매칭(=하단 서명일) 을 채택 →YYYYMMDD. - 성명 추출 — 단순 텍스트 매칭이 아니라 박스 좌표 기반.
생년월일라벨 박스를 앵커로 잡고, ① 같은 행(중심 y 차이 45px 이내) ② 앵커보다 왼쪽(x) 에 있는 ③ 2글자 이상 한글 을 후보로 모아,성명/주소/전화번호등 라벨 단어(LABELS)를 제외한 뒤 가장 왼쪽 후보를 성명으로 확정. - 폴백(2차) — 1차에서 날짜·이름 중 하나라도 못 찾으면 크롭 없이 전체 페이지 재인식. 여기서도 실패하면
[실패]로 로그만 남기고 파일은 그대로 둔다. - 리네임 + 백업 — 원본을
_원본백업/원래이름.pdf로 복사(copy2)해 보관한 뒤os.rename. 동명이인 등 파일명 충돌 시_2,_3자동 부여.
핵심 코드¶
OCR 엔진 (속도 최적화)
def build_engine():
from rapidocr import RapidOCR, LangRec, OCRVersion, ModelType
return RapidOCR(params={
"Rec.lang_type": LangRec.KOREAN,
"Rec.ocr_version": OCRVersion.PPOCRV5,
"Rec.model_type": ModelType.MOBILE,
"Global.use_cls": False, # 문서가 정방향 → 각도분류 생략(속도↑)
"Det.limit_side_len": 640, # 글자 탐지 입력 축소(속도↑)
})
PDF 첫 페이지 → 이미지, 관심영역 크롭
def page_image(pdf_path, dpi=150):
doc = fitz.open(pdf_path)
pix = doc[0].get_pixmap(dpi=dpi)
img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, pix.n)
if pix.n == 4: img = img[:, :, :3] # RGBA -> RGB
elif pix.n == 1: img = np.repeat(img, 3, axis=2) # gray -> RGB
doc.close()
return np.ascontiguousarray(img)
def band(img, top=0.30, bottom=0.85):
H = img.shape[0]
return np.ascontiguousarray(img[int(H*top):int(H*bottom), :, :])
작성일자 추출 (마지막 날짜 채택)
def extract_date(results):
joined = " ".join(txt for _, txt, _ in results)
m = list(re.finditer(r"(\d{4})\s*년\s*(\d{1,2})\s*월\s*(\d{1,2})", joined))
if not m:
return None
y, mo, d = m[-1].groups() # 마지막(하단) 날짜 = 서명일
return "%04d%02d%02d" % (int(y), int(mo), int(d))
성명 추출 (‘생년월일’ 앵커 + 같은 행 왼쪽 셀)
LABELS = {"성","명","성명","생년월일","주소","주","소","전화번호","이용허락","저작물","동의서","성별"}
def extract_name(results):
def center(box):
return (box[0][1] + box[2][1]) / 2.0, (box[0][0] + box[2][0]) / 2.0 # (cy, cx)
anchor = None
for box, txt, _ in results: # 1) '생년월일' 앵커
if "생년" in txt.replace(" ", ""):
anchor = center(box); break
candidates = []
for box, txt, _ in results:
cy, cx = center(box)
hangul = re.sub(r"[^가-힣]", "", txt) # 한글만 남김
if len(hangul) < 2 or hangul in LABELS: # 라벨/노이즈 제거
continue
if anchor:
row_y, birth_x = anchor
if abs(cy - row_y) < 45 and cx < birth_x: # 같은 행 & 생년월일 왼쪽
candidates.append((cx, hangul))
else:
candidates.append((cy, hangul))
if not candidates:
return None
candidates.sort()
return candidates[0][1] # 가장 왼쪽 후보
메인 루프 (1차 밴드 → 2차 전체 폴백 → 백업 후 리네임)
img = page_image(src, dpi=120)
results = to_results(engine(band(img))) # 1차: 크롭 영역만
date, name = extract_date(results), extract_name(results)
if not date or not name: # 2차: 전체 페이지
results = to_results(engine(img))
date, name = extract_date(results), extract_name(results)
if not date or not name:
log(" [실패] %s -> %s 인식 못함" % (f, "/".join(miss))); continue
stem = "%s_%s" % (date, name)
dst = safe_target(stem, ".pdf", BASE, src) # 중복시 _2, _3
os.makedirs(BACKUP, exist_ok=True)
shutil.copy2(src, os.path.join(BACKUP, f)) # 원본 백업
os.rename(src, dst)
exe 대응 (PyInstaller 실행 경로)
def base_dir():
if getattr(sys, "frozen", False): # PyInstaller exe
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__))
사용 패키지 / 설치¶
pip install rapidocr onnxruntime pymupdf numpy opencv-python
pip install pyinstaller
| 패키지 | 버전 | 용도 |
|---|---|---|
| rapidocr | 3.9.1 | OCR (PP-OCRv5 KOREAN mobile) |
| onnxruntime | 1.23.2 | RapidOCR 추론 백엔드(CPU) |
| PyMuPDF (fitz) | 1.26.3 | PDF 첫 페이지 래스터화 |
| numpy | 2.2.0 | 이미지 배열/크롭 |
| opencv-python | 5.0.0.93 | RapidOCR 전처리 의존 |
| pyinstaller | 6.12.0 | exe 빌드 |
빌드
pyinstaller --noconfirm --name PDF_rename rename_pdf.py
# → dist/PDF_rename/ (PDF_rename.exe + _internal) 폴더째 zip 배포
특이사항 / 결정¶
- onedir 배포: RapidOCR ONNX 모델·onnxruntime DLL이 커서 onefile로 묶으면 실행 시마다 임시폴더 압축해제로 느려짐 → 폴더형(onedir) zip 배포 선택.
- 성명은 좌표 기반으로 뽑음: 양식에 ‘성명’ 글자와 실제 이름이 따로 인식되고, 주소/전화 등 다른 한글도 잡히기 때문에 텍스트만으로는 특정 불가. ‘생년월일’ 셀 기준 같은 행 왼쪽 셀이라는 양식의 구조를 이용.
- 날짜는 마지막 매칭 사용: 문서 상단 본문에도 연도 표기가 있어 하단 서명일을 잡기 위함.
- 원본 무손실: 삭제 없이
_원본백업보관 + rename, 재실행 시 이미 변환된 파일은 skip.
Actions