카테고리: 업무 자동화

파이썬 업무 자동화#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로 파일을 열면 끝이에요.

Python
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() 한 줄이면 됩니다.

Python
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 옵션을 쓰면 돼요. 표지만 빼고 붙이는 것도 가능해요.

Python
writer.append("보고서.pdf", pages=(1, 5))   # 2~5쪽만 (0부터 세서 1~4)
writer.merge(position=2, fileobj="표지.pdf")  # 2쪽 뒤에 끼워넣기

📌 3단계 - PDF 쪼개기 (분할 · 원하는 쪽만 추출)

합쳤으면 이제 반대로 쪼개볼게요. 두 가지 방식이 있어요.

① N쪽씩 잘라서 여러 파일로 (분할)

Python
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()를 반복문 안에서 새로 만드는 것이 포인트예요. 밖에 두면 조각들이 계속 누적돼요!

② 원하는 쪽만 콕 집어서 (추출)

Python
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로 안전하게)

스캔본이 옆으로 누워 있으면 회전, 남한테 보낼 파일이면 비밀번호. 둘 다 몇 줄이면 끝나요.

Python
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 로 저장하고 바로 돌려보세요!

Python
# 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(비밀번호) 를 먼저 부르세요.
Python
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()를 빈 상태로 시작할 때만 쓰는 게 안전해요.
Python
# ❌ 이렇게 하면 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를 다룰 때 파이썬 재귀 한계에 걸려요.
  • 해결: 파일 맨 위에 아래 두 줄을 추가하세요.
Python
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으로는 안 긁히던 자바스크립트 페이지, 로그인이 필요한 사이트를 브라우저째로 조종해서 뚫어볼 거예요. 기대해주세요 🙌

궁금한 점이나 안 되는 부분은 댓글로 남겨주세요. 최대한 빨리 답변드릴게요 💬

📚 파이썬 업무 자동화 시리즈

연재 · 14편 중 8번째

파이썬 업무 자동화

  1. #1엑셀 자동화 입문 (openpyxl)!
  2. #2웹 크롤링 입문 (requests + BeautifulSoup)!
  3. #3파일 정리 자동화 (이름 일괄 변경)!
  4. #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)!
  5. #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI)
  6. #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler)
  7. #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium)
  8. #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄지금 읽는 글
  9. #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI)
  10. #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄
  11. #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬
  12. #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗
  13. #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기
  14. #14파워포인트(PowerPoint) 보고서 자동 생성! 엑셀 숫자만 바꾸면 발표자료 완성 (python-pptx) 📊
전체 14편 보기접기

글쓴이

Mojangcam

직접 코드를 짜고, 서버를 돌리고, 막힌 에러를 해결하며 배운 것들을 기록하는 개발·IT 실전 기록 블로그