PDF 파일 여러 개를 자동으로 합치고 분할하기

월말 정산 때마다 거래처에서 온 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를 하나로 합치기

핵심은 PdfWriterappend() 한 줄입니다. 파일 경로를 순서대로 넘기면 그대로 이어 붙습니다.

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.pdfinvoice_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)
pypdf로 PDF 합치기와 분할한 실제 실행 결과
실제 실행입니다. 이름순으로 정렬하면 10장이 2장보다 먼저 붙습니다 — 숫자를 뽑아 정렬해야 의도대로 됩니다.

파일명 안의 숫자만 뽑아 정수로 비교하는 방식입니다. 이 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 도구들을 정리해 보겠습니다.

댓글 남기기