월말 정산 때마다 거래처에서 온 PDF 명세서 서른 장을 하나로 합치고, 200쪽짜리 보고서에서 특정 챕터만 뽑아 메일로 보낸 적이 있을 겁니다. 온라인 변환 사이트에 파일을 하나씩 끌어다 놓고, 광고를 닫고, 다운로드 버튼을 누르는 데 한 건당 3~4분. 이걸 주 2회만 해도 한 달이면 한 시간 가까이 사라집니다. 이번 글에서는 PDF 자동 병합 분할을 파이썬 스크립트와 CLI 도구로 처리하는 방법을 다룹니다. 한 번 만들어두면 서른 개짜리 병합이 1초 안에 끝납니다.
온라인 변환 사이트를 쓰지 말아야 하는 이유
무료 PDF 병합 사이트는 편하지만, 파일이 남의 서버로 올라간다는 점이 문제입니다. 대부분 “몇 시간 뒤 자동 삭제”를 약속하지만 그건 약속일 뿐 검증할 방법이 없습니다.
- 거래명세서·급여대장·계약서에는 사업자번호, 계좌번호, 주민번호 뒷자리가 들어 있는 경우가 많습니다.
- 회사 보안 규정상 외부 클라우드 업로드가 금지된 문서를 무심코 올리면 그 자체로 사고입니다.
- 무엇보다 파일이 열 개만 넘어가도 수작업 업로드가 더 느립니다.
아래 방법들은 전부 내 컴퓨터 안에서만 동작합니다. 네트워크로 나가는 데이터가 없습니다.
준비: pypdf 설치
파이썬 진영의 표준은 pypdf입니다. 예전 자료에 자주 나오는 PyPDF2는 2023년에 개발이 중단되고 pypdf로 다시 합쳐졌습니다. 지금 새로 시작한다면 pypdf만 설치하면 됩니다.
# 파이썬 3.7 이상이면 그대로 됩니다.
pip install pypdf
# 설치 확인
python -c "import pypdf; print(pypdf.__version__)"
순수 파이썬으로 작성된 라이브러리라 별도의 시스템 프로그램을 깔 필요가 없고, 윈도우·맥·리눅스에서 똑같이 동작합니다.
여러 PDF를 하나로 합치기
핵심은 PdfWriter의 append() 한 줄입니다. 파일 경로를 순서대로 넘기면 그대로 이어 붙습니다.
from pathlib import Path
from pypdf import PdfWriter
def merge(pdf_paths, output):
writer = PdfWriter()
for path in pdf_paths:
writer.append(str(path))
with open(output, "wb") as f:
writer.write(f)
writer.close()
if __name__ == "__main__":
folder = Path("./invoices")
files = sorted(folder.glob("*.pdf"))
merge(files, "merged.pdf")
print(f"{len(files)}개 파일을 merged.pdf로 병합했습니다.")
오래된 예제에서 보이는 PdfMerger는 현재 사용이 권장되지 않습니다. PdfWriter가 병합 기능을 그대로 흡수했으니 위 형태를 쓰면 됩니다.
파일 순서가 뒤죽박죽이라면
여기서 초보자가 가장 많이 걸려 넘어집니다. 파이썬의 기본 sorted()는 문자열 순서로 정렬하기 때문에 invoice_10.pdf가 invoice_2.pdf보다 앞에 옵니다. 100장을 합쳐놓고 나중에 순서가 엉킨 걸 발견하면 처음부터 다시 해야 합니다.
import re
from pathlib import Path
def natural_key(path):
"""파일명 안의 숫자를 숫자로 비교해서 1, 2, 10 순서가 되게 한다."""
parts = re.split(r'(\d+)', path.name)
return [int(t) if t.isdigit() else t.lower() for t in parts]
files = sorted(Path("./invoices").glob("*.pdf"), key=natural_key)
for f in files:
print(f.name)

파일명 안의 숫자만 뽑아 정수로 비교하는 방식입니다. 이 natural_key를 위 병합 스크립트의 sorted()에 key로 넘겨주면 사람이 기대하는 순서대로 붙습니다.
큰 PDF를 일정 쪽수로 자르기
메일 첨부 용량 제한 때문에 200쪽짜리 문서를 10쪽씩 나눠야 하는 상황입니다.
from pathlib import Path
from pypdf import PdfReader, PdfWriter
def split_by_size(src, chunk=10, outdir="split"):
reader = PdfReader(src)
Path(outdir).mkdir(exist_ok=True)
total = len(reader.pages)
for start in range(0, total, chunk):
writer = PdfWriter()
for i in range(start, min(start + chunk, total)):
writer.add_page(reader.pages[i])
name = Path(outdir) / f"part_{start // chunk + 1:02d}.pdf"
with open(name, "wb") as f:
writer.write(f)
print(f"{name} 저장 ({min(chunk, total - start)}쪽)")
split_by_size("report.pdf", chunk=10)
part_01.pdf부터 순서대로 저장되고, 마지막 조각은 남은 쪽수만큼만 만들어집니다. chunk 값만 바꾸면 5쪽씩이든 50쪽씩이든 그대로 동작합니다.
특정 구간만 뽑아내기
전체를 자를 필요 없이 “계약서 서명 페이지만” 같은 요청이 더 흔합니다. 이때 주의할 점은 pypdf의 페이지 번호가 0부터 시작한다는 것입니다. PDF 뷰어에 보이는 12쪽은 코드상 11번입니다.
from pypdf import PdfReader, PdfWriter
def extract(src, start_page, end_page, out):
"""start_page와 end_page는 사람이 보는 페이지 번호(1부터)."""
reader = PdfReader(src)
writer = PdfWriter()
for i in range(start_page - 1, end_page):
writer.add_page(reader.pages[i])
with open(out, "wb") as f:
writer.write(f)
# 계약서 12~18쪽만 따로 뽑기
extract("contract.pdf", 12, 18, "contract_signature.pdf")
비밀번호가 걸린 PDF
은행 명세서나 카드 이용내역은 대개 암호가 걸려 있습니다. 이 상태로 병합을 시도하면 FileNotDecryptedError가 납니다. 읽기 전에 한 번 풀어주면 됩니다.
from pypdf import PdfReader
reader = PdfReader("locked.pdf")
if reader.is_encrypted:
result = reader.decrypt("비밀번호")
if result == 0:
raise SystemExit("비밀번호가 틀렸습니다.")
print(len(reader.pages), "쪽")
decrypt()가 0을 돌려주면 비밀번호가 틀린 것입니다. 여러 파일을 일괄 처리할 때는 이 반환값을 꼭 확인해야, 조용히 실패한 파일을 나중에 발견하는 일을 막을 수 있습니다.
코드를 안 쓰고 싶다면: qpdf
파이썬을 건드리기 싫거나, 배치 파일 한 줄로 끝내고 싶을 때는 qpdf가 좋습니다. 오픈소스 CLI 도구이고 윈도우·맥·리눅스 모두 지원합니다.
# 여러 파일을 순서대로 병합
qpdf --empty --pages a.pdf b.pdf c.pdf -- merged.pdf
# 1~10쪽만 뽑기 (점(.)은 "지금 입력 파일"이라는 뜻)
qpdf input.pdf --pages . 1-10 -- first10.pdf
# 10쪽씩 잘라서 out-01.pdf, out-02.pdf ... 로 저장
qpdf --split-pages=10 input.pdf out.pdf
# 파일이 깨졌는지 먼저 검사
qpdf --check input.pdf
특히 --check는 유용합니다. 스캐너나 오래된 프로그램이 만든 PDF는 규격이 어긋난 경우가 있어서, 병합이 실패할 때 원인을 먼저 알려줍니다.
어떤 방법을 고를까
| 도구 | 방식 | 플랫폼 | 비용 | 적합한 경우 |
|---|---|---|---|---|
| pypdf | 파이썬 코드 | 윈도우 / 맥 / 리눅스 | 무료(오픈소스) | 조건 분기·파일명 규칙 등 로직이 필요할 때 |
| qpdf | 명령줄 | 윈도우 / 맥 / 리눅스 | 무료(오픈소스) | 한 줄로 끝나는 단순 병합·분할 |
| Ghostscript | 명령줄 | 윈도우 / 맥 / 리눅스 | 무료(오픈소스) | 병합과 동시에 용량을 줄여야 할 때 |
| 미리보기 앱 | 마우스 조작 | 맥 전용 | 무료(기본 탑재) | 파일 두세 개짜리 일회성 작업 |
반복 작업이라면 pypdf, 어쩌다 한 번이면 qpdf 한 줄이 합리적입니다. 파일이 서너 개뿐이고 다시 할 일이 없다면 굳이 자동화하지 않는 것도 방법입니다.
실제로 사고가 나는 지점
- 원본을 덮어쓰지 마세요. 출력 파일명을 입력과 같게 두면 원본이 사라집니다. 결과는 별도 폴더에 저장하는 습관을 들이는 편이 안전합니다.
- 입력 폴더에 결과 파일을 두면 안 됩니다.
merged.pdf를 같은 폴더에 저장한 뒤 스크립트를 한 번 더 돌리면, 자기 자신까지 다시 합쳐 파일이 두 배로 부풀어 오릅니다. - 서식 입력값과 전자서명은 보존되지 않을 수 있습니다. 병합 과정에서 서명이 무효화되므로, 서명된 문서는 합치기 전에 법적 효력이 필요한지 확인하세요.
- 스캔본은 용량이 급격히 커집니다. 이미지 기반 PDF 30장을 합치면 수백 MB가 되기도 합니다. 이럴 때는 Ghostscript로 다시 압축하는 단계를 붙이는 게 좋습니다.
- 페이지 번호는 0부터입니다. 구간 추출에서 한 쪽씩 밀리는 실수가 가장 흔합니다.
처음에는 폴더 하나, 파일 세 개로만 테스트하세요. 100개짜리 폴더에 처음부터 돌렸다가 순서가 엉키면 확인하는 데만 30분이 걸립니다.
마무리
가장 작은 첫걸음은 이렇습니다. 바탕화면에 pdf_test 폴더를 만들고 PDF 세 개를 넣은 뒤, 위 병합 스크립트에서 경로만 바꿔 한 번 실행해 보세요. 5분이면 끝나고, 그다음부터는 파일이 서른 개여도 똑같이 1초입니다.
여기까지 익숙해졌다면 이 스크립트를 윈도우 작업 스케줄러에 걸어 매주 자동 실행되게 만들 수도 있습니다. 다음 글에서는 터미널 작업 속도를 확 끌어올려 주는 CLI 도구들을 정리해 보겠습니다.