파이썬 업무 자동화#8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄
목차15개 항목
안녕하세요! Mojangcam입니다 😎
지난 파이썬 업무 자동화 #6 - 예약 실행 자동화 (schedule·APScheduler)에서는 스크립트가 정해진 시간에 알아서 돌아가게 만들어봤어요. 이제 실행은 자동인데, 정작 손으로 하는 일이 하나 남아 있죠. 바로 PDF 정리요 😅
스캔한 계약서 12장, 견적서, 영수증… 온라인 PDF 합치기 사이트에 파일을 올려본 적 있으실 거예요. 그런데 그거 회사 계약서인데 남의 서버에 올려도 괜찮은 걸까요? 파일 개수 제한에 걸려서 두 번 세 번 나눠 올린 적은요?
이번 편에서는 그 작업을 내 컴퓨터 안에서 파이썬으로 끝냅니다. 폴더에 PDF를 던져 넣고 스크립트 한 번 실행하면 병합·분할·페이지 추출·회전·비밀번호까지 한 방에 처리돼요. 업로드 0회, 요금 0원 💡
💬 시리즈 순서상 #7(Selenium)이 먼저지만, PDF 편을 먼저 준비했어요. #7 Selenium 편은 바로 다음 편에서 이어집니다!

🧠 왜 웹사이트 말고 파이썬인가요?
- 보안: 계약서·주민등록등본 같은 파일을 외부 서버에 올릴 필요가 없어요. 전부 로컬에서 처리됩니다.
- 무제한: 파일 개수·용량 제한이 없어요. 100개든 500개든 똑같이 1초.
- 반복 가능: 한 번 만들어두면 매달 정산 자료 묶는 데 계속 써먹어요. #6에서 배운 예약 실행이랑 붙이면 완전 자동이고요.
- 무료: pypdf는 오픈소스라 결제 화면이 안 뜹니다 🙌
📌 준비물
- Python 3.10 이상 (python-dotenv 1.2부터 3.10+ 필요해요. pypdf 자체는 3.9+에서 돌아갑니다)
- pypdf 6.x — 작성 기준 최신은 6.16.2 (2026-08 기준)
- python-dotenv 1.x — 비밀번호를
.env에서 불러올 때 사용 (작성 기준 1.2.3) - 연습용 PDF 파일 2~3개
pip install "pypdf[crypto]" python-dotenv💡 pypdf[crypto]처럼 대괄호를 꼭 붙여주세요. 그냥 pip install pypdf만 하면 AES 암호화를 걸 때 에러가 납니다. (아래 '자주 나는 에러' 3번 참고)
💬 예전에 PyPDF2로 검색해서 배우신 분들 많으실 텐데요, PyPDF2는 2023년에 pypdf로 이름이 바뀌고 개발이 통합됐어요. 지금 새로 시작한다면 무조건 pypdf입니다!
폴더 구조는 이렇게 잡을게요
my_pdf_tool/
├── pdf_tool.py ← 우리가 만들 스크립트
├── .env ← 비밀번호 보관 (깃허브에 절대 올리지 마세요!)
├── input_pdfs/ ← 원본 PDF를 여기에 넣어요
│ ├── 01_계약서.pdf
│ ├── 02_견적서.pdf
│ └── 03_영수증.pdf
└── output_pdfs/ ← 결과물이 여기에 생겨요 (자동 생성).env 파일은 이렇게 딱 한 줄이면 돼요.
PDF_PASSWORD=여기에_원하는_비밀번호⚠️ .env는 반드시 .gitignore에 추가하세요. 깃허브에 비밀번호를 통째로 올리는 사고, 생각보다 정말 많이 납니다 😱
.env
output_pdfs/📌 1단계 - PDF 열어서 쪽수부터 확인하기
합치기 전에 원본이 몇 쪽짜리인지부터 봐야겠죠. PdfReader로 파일을 열면 끝이에요.
from pathlib import Path
from pypdf import PdfReader
def show_info(pdf_path: Path) -> int:
reader = PdfReader(pdf_path)
title = (reader.metadata.title if reader.metadata else None) or "(제목 없음)"
print(f"📄 {pdf_path.name} | {len(reader.pages)}쪽 | 제목: {title}")
return len(reader.pages)
for pdf in sorted(Path("input_pdfs").glob("*.pdf")):
show_info(pdf)하나씩 뜯어볼게요 💡
- PdfReader : PDF를 읽기 전용으로 여는 클래스예요. 원본은 절대 안 건드립니다.
- reader.pages : 페이지들이 담긴 리스트예요.
len()으로 쪽수를,reader.pages[0]으로 첫 장을 꺼내요. 참고로 0부터 시작합니다. - reader.metadata : 제목·작성자 같은 문서 정보예요. 없는 PDF도 많아서
None체크를 꼭 해줘야 해요. - Path.glob("*.pdf") : 폴더 안 PDF를 전부 찾아줘요.
sorted()로 감싸면 파일명 순서가 보장돼요. 그래서 파일 이름을01_,02_처럼 지어두는 게 중요해요!
📌 2단계 - 폴더 안 PDF 전부 합치기 (병합)
드디어 본론이에요. pypdf에서 병합은 PdfWriter.append() 한 줄이면 됩니다.
from pypdf import PdfWriter
def merge_folder(in_dir: Path, out_path: Path) -> None:
files = sorted(in_dir.glob("*.pdf"))
if not files:
print("⚠️ 합칠 PDF가 없어요.")
return
writer = PdfWriter()
for f in files:
writer.append(str(f), outline_item=f.stem) # 파일명이 북마크로!
writer.write(out_path)
print(f"✅ 병합 완료: {out_path.name} → {len(writer.pages)}쪽")하나씩 뜯어볼게요 💡
- PdfWriter : 새 PDF를 만드는 클래스예요. 읽기는 Reader, 쓰기는 Writer 이렇게 짝으로 기억하시면 돼요.
- append(파일경로) : 그 PDF를 통째로 뒤에 이어 붙여요. 반복문으로 몇 개든 붙일 수 있어요.
- outline_item=f.stem : 이게 숨은 꿀팁이에요! 합본을 열었을 때 왼쪽에 북마크(목차)가 생겨서
01_계약서,02_견적서를 클릭 한 번으로 이동할 수 있어요.f.stem은 확장자를 뺀 파일 이름이에요. - write(경로) : 실제 파일로 저장해요. 이걸 안 부르면 아무 일도 안 일어납니다 😅
💡 일부 쪽만 붙이고 싶다면? pages 옵션을 쓰면 돼요. 표지만 빼고 붙이는 것도 가능해요.
writer.append("보고서.pdf", pages=(1, 5)) # 2~5쪽만 (0부터 세서 1~4)
writer.merge(position=2, fileobj="표지.pdf") # 2쪽 뒤에 끼워넣기
📌 3단계 - PDF 쪼개기 (분할 · 원하는 쪽만 추출)
합쳤으면 이제 반대로 쪼개볼게요. 두 가지 방식이 있어요.
① N쪽씩 잘라서 여러 파일로 (분할)
def split_every(src: Path, out_dir: Path, size: int = 1) -> None:
reader = PdfReader(src)
total = len(reader.pages)
for start in range(0, total, size):
end = min(start + size, total)
writer = PdfWriter()
for i in range(start, end):
writer.add_page(reader.pages[i])
out_path = out_dir / f"{src.stem}_p{start + 1}-{end}.pdf"
writer.write(out_path)
print(f"✂️ 분할: {out_path.name}")- range(0, total, size) : 0, 4, 8… 이렇게
size씩 건너뛰며 시작점을 잡아요. - min(start + size, total) : 마지막 조각이 남은 쪽수보다 커지지 않게 막아주는 안전장치예요. 10쪽을 4쪽씩 자르면 4·4·2로 딱 떨어져요.
- add_page(페이지객체) : 페이지를 한 장씩 넣어요.
PdfWriter()를 반복문 안에서 새로 만드는 것이 포인트예요. 밖에 두면 조각들이 계속 누적돼요!
② 원하는 쪽만 콕 집어서 (추출)
def extract_pages(src: Path, out_path: Path, pages) -> None:
writer = PdfWriter()
writer.append(str(src), pages=[p - 1 for p in pages])
writer.write(out_path)
print(f"📎 추출 완료: {out_path.name} (원본 {pages}쪽)")
# 1, 2, 6쪽만 뽑아서 요약본 만들기
extract_pages(Path("output_pdfs/전체_합본.pdf"), Path("output_pdfs/요약본.pdf"), [1, 2, 6])- [p - 1 for p in pages] : 사람은 1쪽부터 세지만 파이썬은 0부터 세죠. 그래서 1씩 빼주는 거예요. 이렇게 해두면 함수를 쓸 때 눈에 보이는 쪽 번호 그대로 넣으면 돼서 헷갈릴 일이 없어요!
📌 4단계 - 회전하고 비밀번호 걸기 (.env로 안전하게)
스캔본이 옆으로 누워 있으면 회전, 남한테 보낼 파일이면 비밀번호. 둘 다 몇 줄이면 끝나요.
def rotate_all(src: Path, out_path: Path, angle: int = 90) -> None:
writer = PdfWriter(clone_from=str(src)) # ⚠️ with랑 같이 쓰면 안 돼요!
for page in writer.pages:
page.rotate(angle)
writer.write(out_path)
def protect(src: Path, out_path: Path, password) -> None:
if not password:
print("⚠️ .env에 PDF_PASSWORD가 없어요. 건너뜁니다.")
return
writer = PdfWriter(clone_from=str(src))
writer.encrypt(password, algorithm="AES-256")
writer.write(out_path)
print(f"🔒 암호 설정 완료: {out_path.name}")하나씩 뜯어볼게요 💡
- PdfWriter(clone_from=경로) : 원본을 통째로 복사한 상태로 Writer를 시작해요. "원본 그대로 두고 살짝만 고치기"에 딱이에요.
- page.rotate(각도) : 90의 배수만 됩니다. 45도 같은 건 안 돼요. 음수(-90)를 넣으면 반대로 돌아가요.
- encrypt(비밀번호, algorithm="AES-256") : 열기 비밀번호를 걸어요. AES-256이 현재 가장 강력한 방식이고, 이걸 쓰려면 아까
pypdf[crypto]로 설치했어야 해요. - password는 절대 코드에 적지 않아요.
.env에서os.getenv("PDF_PASSWORD")로 불러옵니다. 코드를 남에게 보여주거나 깃허브에 올려도 비밀번호는 안 새어나가요 🔐
📌 전체 코드 (복붙 OK)
pdf_tool.py 로 저장하고 바로 돌려보세요!
# pypdf 6.x / python-dotenv 1.x / Python 3.10+
# 작성 기준: pypdf 6.16.2, python-dotenv 1.2.3 (2026-08)
import os
from pathlib import Path
from dotenv import load_dotenv
from pypdf import PdfReader, PdfWriter
load_dotenv() # .env 파일 읽어오기
PDF_PASSWORD = os.getenv("PDF_PASSWORD") # 비밀번호는 코드에 절대 안 적어요!
IN_DIR = Path("input_pdfs") # 원본 PDF들이 들어 있는 폴더
OUT_DIR = Path("output_pdfs") # 결과물이 저장될 폴더
OUT_DIR.mkdir(exist_ok=True)
def show_info(pdf_path: Path) -> int:
"""PDF 한 개의 쪽수와 제목을 확인해요."""
reader = PdfReader(pdf_path)
title = (reader.metadata.title if reader.metadata else None) or "(제목 없음)"
print(f"📄 {pdf_path.name} | {len(reader.pages)}쪽 | 제목: {title}")
return len(reader.pages)
def merge_folder(in_dir: Path, out_path: Path) -> None:
"""폴더 안의 모든 PDF를 파일명 순서대로 하나로 합쳐요."""
files = sorted(in_dir.glob("*.pdf"))
if not files:
print("⚠️ 합칠 PDF가 없어요.")
return
writer = PdfWriter()
for f in files:
writer.append(str(f), outline_item=f.stem) # 파일명이 북마크로 들어가요
writer.write(out_path)
print(f"✅ 병합 완료: {out_path.name} (파일 {len(files)}개 → {len(writer.pages)}쪽)")
def split_every(src: Path, out_dir: Path, size: int = 1) -> None:
"""PDF를 size쪽씩 잘라서 여러 파일로 나눠요."""
reader = PdfReader(src)
total = len(reader.pages)
for start in range(0, total, size):
end = min(start + size, total)
writer = PdfWriter()
for i in range(start, end):
writer.add_page(reader.pages[i])
out_path = out_dir / f"{src.stem}_p{start + 1}-{end}.pdf"
writer.write(out_path)
print(f"✂️ 분할: {out_path.name}")
def extract_pages(src: Path, out_path: Path, pages) -> None:
"""원하는 쪽만 뽑아 새 PDF로 저장해요. (pages는 사람이 세는 1부터!)"""
writer = PdfWriter()
writer.append(str(src), pages=[p - 1 for p in pages])
writer.write(out_path)
print(f"📎 추출 완료: {out_path.name} (원본 {pages}쪽)")
def rotate_all(src: Path, out_path: Path, angle: int = 90) -> None:
"""모든 페이지를 angle도 회전시켜요. (90의 배수만 가능)"""
writer = PdfWriter(clone_from=str(src)) # ⚠️ clone_from은 with와 같이 쓰지 마세요!
for page in writer.pages:
page.rotate(angle)
writer.write(out_path)
print(f"🔄 회전 완료: {out_path.name} ({angle}도)")
def protect(src: Path, out_path: Path, password) -> None:
"""PDF에 열기 비밀번호를 걸어요."""
if not password:
print("⚠️ .env에 PDF_PASSWORD가 없어요. 건너뜁니다.")
return
writer = PdfWriter(clone_from=str(src))
writer.encrypt(password, algorithm="AES-256")
writer.write(out_path)
print(f"🔒 암호 설정 완료: {out_path.name}")
if __name__ == "__main__":
print("=" * 45)
for pdf in sorted(IN_DIR.glob("*.pdf")):
show_info(pdf)
print("=" * 45)
merged = OUT_DIR / "전체_합본.pdf"
merge_folder(IN_DIR, merged)
split_every(merged, OUT_DIR, size=4)
extract_pages(merged, OUT_DIR / "요약본.pdf", pages=[1, 2, 6])
rotate_all(merged, OUT_DIR / "합본_회전.pdf", angle=90)
protect(merged, OUT_DIR / "합본_잠김.pdf", PDF_PASSWORD)
print("=" * 45)
print("🎉 모든 작업이 끝났어요!")📌 실행 결과
input_pdfs 폴더에 5쪽·3쪽·2쪽짜리 PDF 3개를 넣고 돌려봤어요.
=============================================
📄 01_계약서.pdf | 5쪽 | 제목: (제목 없음)
📄 02_견적서.pdf | 3쪽 | 제목: (제목 없음)
📄 03_영수증.pdf | 2쪽 | 제목: (제목 없음)
=============================================
✅ 병합 완료: 전체_합본.pdf (파일 3개 → 10쪽)
✂️ 분할: 전체_합본_p1-4.pdf
✂️ 분할: 전체_합본_p5-8.pdf
✂️ 분할: 전체_합본_p9-10.pdf
📎 추출 완료: 요약본.pdf (원본 [1, 2, 6]쪽)
🔄 회전 완료: 합본_회전.pdf (90도)
🔒 암호 설정 완료: 합본_잠김.pdf
=============================================
🎉 모든 작업이 끝났어요!5+3+2 = 10쪽 합본이 만들어졌고, 4쪽씩 자르니 4·4·2로 세 조각이 나왔어요. 합본_잠김.pdf를 열어보면 비밀번호 입력창이 뜨고, .env에 적어둔 비밀번호를 넣어야만 열립니다 🔒


📌 자주 나는 에러
1. ModuleNotFoundError: No module named 'pypdf'
- 원인: 라이브러리를 설치 안 했거나, 가상환경을 켜지 않은 상태로 실행했어요.
- 해결:
pip install "pypdf[crypto]" python-dotenv를 실행하세요. 가상환경(venv)을 쓴다면 활성화 후 설치해야 해요.
2. ModuleNotFoundError: No module named 'PyPDF2'
- 원인: 오래된 블로그 글을 보고
from PyPDF2 import ...라고 적으셨어요. - 해결: PyPDF2는 2023년에 pypdf로 통합됐어요.
from pypdf import PdfReader, PdfWriter로 바꾸면 끝이에요. (PdfFileReader→PdfReader,PdfFileWriter→PdfWriter로 이름도 짧아졌어요)
3. DependencyError: cryptography>=3.1 is required for AES algorithm
- 원인:
pip install pypdf만 해서 암호화 모듈이 빠졌어요. - 해결:
pip install "pypdf[crypto]"로 다시 설치하세요. 따옴표를 안 붙이면 터미널(zsh 등)이 대괄호를 명령어로 착각하니 꼭 따옴표로 감싸세요!
4. FileNotDecryptedError: File has not been decrypted
- 원인: 비밀번호가 걸린 PDF를 그냥 읽으려고 했어요.
- 해결: 읽기 전에
reader.decrypt(비밀번호)를 먼저 부르세요.
reader = PdfReader("합본_잠김.pdf")
if reader.is_encrypted:
reader.decrypt(os.getenv("PDF_PASSWORD"))
print(len(reader.pages))5. 결과 PDF가 0쪽짜리 빈 파일로 나와요 (제일 많이 당하는 함정!)
- 원인:
with PdfWriter(clone_from=...) as writer:처럼 clone_from을with문과 같이 썼어요. pypdf의with진입 시점에 내부적으로 초기화가 일어나서 복사해둔 페이지가 전부 날아가요. - 해결:
clone_from을 쓸 때는with없이 그냥 변수에 담아 쓰세요.with는PdfWriter()를 빈 상태로 시작할 때만 쓰는 게 안전해요.
# ❌ 이렇게 하면 0쪽 빈 파일이 나와요
with PdfWriter(clone_from="원본.pdf") as writer:
writer.write("결과.pdf")
# ✅ 이렇게 쓰세요
writer = PdfWriter(clone_from="원본.pdf")
writer.write("결과.pdf")6. 파일이 이상한 순서로 합쳐져요
- 원인: 파일명이
1.pdf,2.pdf,10.pdf라면 문자열 정렬로1 → 10 → 2순서가 돼요. - 해결: 파일명 앞에 자릿수를 맞춘 번호를 붙이세요.
01.pdf,02.pdf,10.pdf처럼요.
7. RecursionError: maximum recursion depth exceeded
- 원인: 아주 크고 복잡한 PDF를 다룰 때 파이썬 재귀 한계에 걸려요.
- 해결: 파일 맨 위에 아래 두 줄을 추가하세요.
import sys
sys.setrecursionlimit(sys.getrecursionlimit() * 5)💡 보너스 - 이렇게 써먹어요
- 매달 정산 자료 묶기:
input_pdfs에 영수증을 모아두고 월말에 스크립트 한 번 → 합본 완성. #6 예약 실행이랑 붙이면 매월 1일에 알아서 돌아가요. - 메일 보낼 때 자동 암호화:
protect()로 잠근 뒤 파이썬 이메일 자동 발송에 첨부하면 "비밀번호 거는 걸 깜빡했네요 😅" 메일을 다시 보낼 일이 없어요. - PDF 텍스트 뽑아내기:
page.extract_text()한 줄이면 본문이 문자열로 나와요. #5 로컬 LLM에 넘겨서 계약서 요약을 시켜보는 것도 재밌어요! - 작업 끝나면 알림 받기: #4 디스코드 웹훅을 붙이면 "PDF 120개 병합 완료 ✅" 알림이 폰으로 와요.
📌 마무리
오늘은 pypdf로 PDF를 자유자재로 주무르는 법을 배웠어요! 정리하면 이렇습니다.
- 읽기는 PdfReader, 쓰기는 PdfWriter — 이 짝만 기억하면 절반은 끝
- 병합은 append(), 분할은 add_page(), 추출은 append(pages=[...])
- 원본을 살짝 고칠 땐 clone_from (단,
with와 같이 쓰면 안 돼요!) - 비밀번호는 코드가 아니라 .env에, 그리고
.env는.gitignore에 🔐
이제 온라인 PDF 합치기 사이트는 안 들어가셔도 돼요 😎
다음 편은 예고했던 파이썬 업무 자동화 #7 - 동적 페이지·로그인 자동화 (Selenium) 입니다! #2에서 배운 requests+BeautifulSoup으로는 안 긁히던 자바스크립트 페이지, 로그인이 필요한 사이트를 브라우저째로 조종해서 뚫어볼 거예요. 기대해주세요 🙌
궁금한 점이나 안 되는 부분은 댓글로 남겨주세요. 최대한 빨리 답변드릴게요 💬
📚 파이썬 업무 자동화 시리즈
- #1 - 엑셀 자동화 입문 (openpyxl)
- #2 - 웹 크롤링 입문 (requests + BeautifulSoup)
- #3 - 파일 정리 자동화 (이름 일괄 변경)
- #4 - 디스코드 웹훅(Webhook)으로 알림 보내기
- #5 - 로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기
- #6 - 예약 실행 자동화 (schedule·APScheduler)
- #7 - 동적 페이지·로그인 자동화 (Selenium) — 다음 편 예정
- #8 - PDF 자동 처리 (병합·분할·비밀번호) ← 지금 이 글!
- #9 - 마우스·키보드 GUI 자동화 (PyAutoGUI) — 예정
연재 · 14편 중 8번째
파이썬 업무 자동화
- #1엑셀 자동화 입문 (openpyxl)!
- #2웹 크롤링 입문 (requests + BeautifulSoup)!
- #3파일 정리 자동화 (이름 일괄 변경)!
- #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)!
- #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI)
- #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler)
- #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium)
- #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄지금 읽는 글
- #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI)
- #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄
- #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬
- #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗
- #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기
- #14파워포인트(PowerPoint) 보고서 자동 생성! 엑셀 숫자만 바꾸면 발표자료 완성 (python-pptx) 📊
