Application/Python

[Python] 10. 파이썬으로 PDF 합치고 나누기 (PyPDF2)

devsalix 2026. 7. 2. 14:37
728x90

파이썬으로 PDF 합치고 나누기 (PyPDF2)

계약서 하나 만드는데 본문 PDF, 별첨 PDF, 서명란 스캔본 PDF가 따로따로 세 개.

이걸 하나로 묶으려고 뷰어에서 하나씩 열고, 인쇄 눌러서 PDF로 다시 내보내고... 파일이 열 개쯤 되면 그냥 야근 확정이에요. 저도 예전에 이거 손으로 하다가 순서 꼬여서 처음부터 다시 한 적 있거든요.

지금까지 엑셀만 실컷 다뤘잖아요. 근데 막상 회사 문서를 열어보면 계약서, 명세서, 스캔본까지 죄다 PDF예요. 그래서 이번부터는 이 PDF 뭉치를 파이썬으로 합치고, 필요한 페이지만 쏙 뽑아내는 걸 해볼 거예요. 클릭 노가다는 이제 그만.


PyPDF2? pypdf? 대체 뭘 설치하나요

이게 이 글에서 제일 헷갈리는 부분이라 먼저 딱 짚고 갈게요.

블로그나 유튜브에서 검색하면 죄다 PyPDF2라고 나와요. 근데 정작 설치할 땐 pip install pypdf를 하라고 하죠. "어? 이름이 다른데?" 하고 여기서 많이들 멈추시더라고요.

결론만 말하면 이래요.

PyPDF2는 이제 pypdf라는 이름으로 통합됐어요. 검색은 PyPDF2로 하되, 설치와 코드는 pypdf로 쓰면 됩니다.

원래 PyPDF에서 PyPDF2, 3, 4로 갈라졌다가 지금은 다시 pypdf 하나로 합쳐졌거든요. 같은 뿌리라고 보시면 돼요. 새로 배우는 우리는 그냥 최신인 pypdf만 쓰면 그만이에요.

pip install pypdf

 

엑셀 파트에서 pip install openpyxl 해보셨으니 이 흐름은 이제 익숙하시죠.

그리고 하나 더. 인터넷 옛날 예제에는 PdfFileReader 같은 이름이 나와요. pypdf에선 이게 바뀌었어요. File이 끼어 있거나, getPage 같은 카멜케이스면 옛날 문법이라고 보시면 됩니다.

옛날 (PyPDF2) 요즘 (pypdf)
PdfFileReader PdfReader
PdfFileWriter PdfWriter
reader.getNumPages() len(reader.pages)
reader.getPage(0) reader.pages[0]
writer.addPage(p) writer.add_page(p)

 

옛 예제 따라 하다 에러 나면 십중팔구 이 표에서 하나 안 바꿔서예요. 나중에 에러 편에서 또 나올 거니까 이건 눈에만 익혀두세요.


PDF 열고 페이지 수부터 확인하기

PDF를 다룰 때 도구가 딱 두 개예요. 이 그림만 잡으면 나머지는 술술 풀려요.

  • PdfReader = 서류철 펼쳐서 보는 도구 (읽기만, 원본은 안 건드림)
  • PdfWriter = 새 빈 클리어파일 (여기에 필요한 페이지를 끼워넣고 저장)

먼저 읽는 쪽부터 볼게요.

from pypdf import PdfReader

reader = PdfReader("계약서.pdf")                    # PDF 열기 (읽기 전용)
print(f"이 PDF는 총 {len(reader.pages)}페이지예요")  # 페이지 개수 세기

first = reader.pages[0]                             # 1페이지 (인덱스는 0부터!)
print("첫 페이지 잘 열림:", first)                   # 페이지 객체가 나오면 성공

# 실행 결과:
# 이 PDF는 총 5페이지예요
# 첫 페이지 잘 열림: {'/Type': '/Page' ...}

 

여기서 주의 하나. reader.pages[0]1페이지예요. 사람은 1장부터 세는데 파이썬은 0장부터 세거든요. 엑셀 파트에서 인덱스 0 때문에 한 번 고생하셨죠? PDF도 똑같아요. "3페이지 뽑아줘"는 pages[2]가 됩니다. 이거 하나만 기억하면 돼요.


여러 PDF 하나로 합치기 (PDF 합치기)

이제 스테이플러로 서류 묶는 걸 해볼게요. reader에서 페이지를 꺼내서 writer에 한 장씩 얹으면 끝이에요.

from pypdf import PdfReader, PdfWriter

writer = PdfWriter()                      # 빈 새 PDF (클리어파일 꺼내기)

for name in ["문서A.pdf", "문서B.pdf"]:   # 합칠 파일들 순서대로
    reader = PdfReader(name)              # 파일 하나 열고
    for page in reader.pages:             # 그 파일의 모든 페이지를
        writer.add_page(page)            # 새 PDF에 한 장씩 추가

writer.write("합본.pdf")                  # 저장 (원본은 그대로!)
print("합치기 완료: 합본.pdf")

# 실행 결과:
# 합치기 완료: 합본.pdf

 

writer.write("합본.pdf") 이렇게 파일명만 주면 저장돼요. 참 쉽죠.

혹시 "원본 PDF가 날아가면 어떡하지" 걱정되세요? 안심하세요. reader는 읽기만 하고, 결과는 새 파일명(합본.pdf)으로 따로 저장되거든요. 원본은 손 하나 안 댑니다. 단, 결과 파일명을 원본과 똑같이 주면 덮어써지니 그것만 다르게 해주세요.


폴더 안 PDF를 전부 한 번에 합치기

파일이 열 개, 스무 개면 위에처럼 이름을 하나하나 적을 순 없잖아요. 이럴 땐 폴더째로 긁어오면 돼요. auto-02에서 배운 glob, 엑셀 합칠 때 쓴 그 방법 그대로예요.

그리고 여기선 append를 쓸게요. add_page가 페이지 한 장씩 넣는 거라면, append는 파일을 통째로 뒤에 이어붙이는 거예요.

from pathlib import Path
from pypdf import PdfWriter

writer = PdfWriter()

files = sorted(Path("계약서모음").glob("*.pdf"))  # 폴더의 PDF 전부 (이름순 정렬)
for file in files:
    writer.append(str(file))              # 파일 통째로 뒤에 이어붙이기
    print(f"추가: {file.name}")

writer.write("전체합본.pdf")              # 하나로 저장
print(f"{len(files)}개 파일을 하나로 합쳤어요")

# 실행 결과:
# 추가: 계약서01.pdf
# 추가: 계약서02.pdf
# 추가: 계약서03.pdf
# 3개 파일을 하나로 합쳤어요

 

옛날 예제엔 PdfMerger라는 별도 도구가 나오기도 하는데, 요즘은 그냥 Writer의 append 쓰면 됩니다. 도구 하나 덜 외워도 되니 편하죠.


필요한 페이지만 쏙 뽑기 (PDF 나누기)

이번엔 복사기로 필요한 장만 뽑는 쪽이에요. 20장짜리 보고서에서 요약 몇 장만 필요할 때 있잖아요.

from pypdf import PdfReader, PdfWriter

reader = PdfReader("보고서.pdf")          # 원본 열기
writer = PdfWriter()                      # 뽑은 페이지 담을 새 PDF

writer.add_page(reader.pages[0])          # 1페이지 (인덱스 0)
writer.add_page(reader.pages[2])          # 3페이지 (인덱스 2)

writer.write("요약본.pdf")                # 1, 3페이지만 담긴 새 PDF
print("필요한 페이지만 뽑았어요")

# 실행 결과:
# 필요한 페이지만 뽑았어요

 

주석에 페이지 번호를 계속 병기해놨어요. pages[2]가 3페이지. 헷갈리면 이거 다시 보세요.

연속된 범위를 자르고 싶으면 리스트 자르듯 슬라이싱하면 돼요.

for page in reader.pages[0:5]:            # 인덱스 0~4 = 1~5페이지
    writer.add_page(page)                # 앞 5장을 순서대로 담기

 

pages[0:5]면 앞 5장, pages[5:]면 6페이지부터 끝까지. 리스트 다루던 감 그대로 쓰시면 됩니다.


실전: 계약서 여러 개 합치기 (암호 방어까지)

조각 예제 봤으니 실무에 바로 쓸 버전으로 묶어볼게요. 입력 폴더 따로, 결과 폴더 따로 두는 건 엑셀 파트에서 들인 습관 그대로예요. 원본이랑 결과가 섞이면 나중에 헷갈리거든요.

from pathlib import Path
from pypdf import PdfReader, PdfWriter

INPUT = "계약서모음"                        # 입력 폴더
OUTPUT = "결과/계약서_합본.pdf"             # 출력은 다른 폴더 (원본과 분리)
Path("결과").mkdir(exist_ok=True)          # 결과 폴더 없으면 만들기

writer = PdfWriter()
for file in sorted(Path(INPUT).glob("*.pdf")):  # 폴더 안 PDF 전부 (이름순)
    reader = PdfReader(str(file))
    if reader.is_encrypted:                # 암호 걸린 PDF면
        reader.decrypt("")                 # 빈 암호로 해제 시도
    for page in reader.pages:
        writer.add_page(page)             # 페이지 전부 새 PDF에 추가
    print(f"합침: {file.name} ({len(reader.pages)}p)")

writer.write(OUTPUT)                       # 결과 폴더에 저장
print(f"완료! {OUTPUT}")

# 실행 결과:
# 합침: 계약서01.pdf (3p)
# 합침: 계약서02.pdf (5p)
# 완료! 결과/계약서_합본.pdf

 

회사 계약서 중엔 암호 걸린 게 종종 있어요. 그런 건 열자마자 에러가 나거든요. 그래서 reader.is_encrypted(암호가 걸려 있는지 확인)로 미리 살펴보고 decrypt로 풀어주는 방어를 넣어뒀어요. 비밀번호를 안다면 decrypt("비밀번호")로 넣으면 됩니다.


자주 만나는 에러

처음엔 이런 데서 많이 막히시더라고요.

AttributeError: 'PdfReader' object has no attribute 'getNumPages'
인터넷 옛 예제의 PyPDF2 문법을 그대로 쓴 경우예요. getNumPages, getPage, addPage 같은 카멜케이스가 보이면 위쪽 변환표대로 len(reader.pages), reader.pages[0], add_page로 바꿔주세요.

IndexError: list index out of range
5페이지짜리 PDF에서 reader.pages[5]를 뽑으려 한 거예요. 마지막 페이지는 인덱스 4거든요. "3페이지"를 pages[3]으로 적는 실수도 여기 포함이에요. 3페이지는 pages[2]입니다.

PermissionError: [Errno 13] Permission denied
저장하려는 결과 PDF를 이미 뷰어(엣지, 크롬, 아크로뱃)로 열어둔 상태예요. 엑셀 파트에서 겪은 그 패턴 똑같죠. 창 닫고 다시 실행하면 됩니다.


직접 해보기

  1. 워드나 한글에서 아무 문서 2~3개를 "PDF로 저장"해서 계약서모음/ 폴더에 넣어보세요. (실습용 PDF가 없으면 이렇게 만들면 돼요.)
  2. 위 실전 예제를 실행해서 하나로 합쳐보세요. 결과/ 폴더에 합본이 생기면 성공.
  3. 합본에서 1~2페이지만 다시 뽑아 새 PDF로 저장해보세요.

힌트: 3번은 "필요한 페이지만 뽑기" 예제에서 페이지 번호만 바꾸면 돼요. 슬라이싱을 pages[3:]로 바꾸면 "앞 3장 빼고 저장"도 됩니다.


 

합치고 나누는 건 페이지를 통째로 옮기는 거라, 안에 글자가 있든 사진이든 상관없이 잘 됐어요.

근데 진짜 필요한 건 PDF 안의 '내용'을 꺼내 쓰는 거잖아요. 명세서 PDF에서 금액만 뽑아서 엑셀에 정리한다든지. 다음 글에서는 PDF 안의 텍스트를 추출해서 엑셀로 옮기는 법으로 가볼게요. 아, 그리고 스캔본 PDF는 글자가 아니라 '사진'이라서 그냥은 안 뽑히는데, 그 이유도 그때 같이 풀어드릴게요.


 


제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀

 

 
728x90