← ~/notes · 2 min read

PDF 챕터별 자동 분할 — 발표 자료 만들 때 한 챕터만 추출

리버싱 핵심원리 PDF 600페이지 중 20장 인라인 패치 부분만 따로 빼고 싶었다. 5분 스크립트로 끝.

목차
  1. 한 파일 스크립트
  2. 왜 pypdf인가
  3. 북마크 없는 PDF는?
  4. 잡힌 함정
  5. 한 줄 결론

발표 준비할 때 두꺼운 PDF에서 한 챕터만 추출해야 하는 경우가 자주 있다. “리버싱 핵심원리” 책 PDF에서 20장 인라인 패치 부분만 따로 빼서 발표 자료에 넣는 식.

수동으로 페이지 번호 일일이 적어서 나누는 건 노답. 5분이면 끝나는 스크립트 쓰자.


한 파일 스크립트

#!/usr/bin/env python3
"""
PDF를 목차(bookmark)별로 분할.
사용: python split_pdf.py input.pdf
"""
import sys
import re
from pathlib import Path
from pypdf import PdfReader, PdfWriter

def safe_filename(s: str) -> str:
    s = re.sub(r'[<>:"/\\|?*]', '_', s)
    return re.sub(r'\s+', '_', s.strip())[:80]

def split_by_outline(pdf_path: Path, out_dir: Path):
    reader = PdfReader(pdf_path)
    out_dir.mkdir(parents=True, exist_ok=True)

    # 최상위 북마크만 (챕터 단위)
    outline = reader.outline
    chapters = []
    for item in outline:
        if isinstance(item, list):
            continue  # 하위 섹션은 스킵
        page_num = reader.get_destination_page_number(item)
        chapters.append((item.title, page_num))

    # 챕터별 페이지 범위
    for i, (title, start) in enumerate(chapters):
        end = chapters[i+1][1] if i+1 < len(chapters) else len(reader.pages)
        writer = PdfWriter()
        for p in range(start, end):
            writer.add_page(reader.pages[p])

        out_name = f"{i+1:02d}_{safe_filename(title)}.pdf"
        with open(out_dir / out_name, 'wb') as f:
            writer.write(f)
        print(f"  → {out_name}  (p.{start+1}-{end})")

if __name__ == '__main__':
    src = Path(sys.argv[1])
    out = src.parent / src.stem
    split_by_outline(src, out)
pip install pypdf
python split_pdf.py 리버싱_핵심원리.pdf

결과:

리버싱_핵심원리/
├── 01_1장_리버싱_입문.pdf       (p.1-23)
├── 02_2장_Hello_World.pdf       (p.24-58)
├── ...
├── 20_20장_인라인_패치_실습.pdf  (p.487-512)
└── ...

왜 pypdf인가

대안과 비교:

라이브러리의존성속도북마크 지원
pypdf순수 Python느림✓
pdfplumber큼보통△
PyMuPDF (fitz)C 바인딩빠름✓
pdftk (외부 CLI)Java빠름✓

pypdf는 의존성 없는 게 강점. 5분 짜리 스크립트엔 이게 정답.

큰 PDF(>500MB) 자주 쓰면 PyMuPDF로 바꾸면 10배 빨라진다.


북마크 없는 PDF는?

스캔본 PDF 등 북마크가 없으면 위 스크립트 안 통한다. 두 가지 옵션:

  1. 수동 페이지 범위로 자르기 — writer.add_page(reader.pages[p])에서 p 범위만 박으면 됨
  2. OCR + 패턴 매칭 — 각 페이지 첫 줄을 OCR해서 “20장” 같은 헤더로 시작하는 페이지 자동 감지. pytesseract + 정규식.

대부분의 e-book/교과서 PDF는 북마크가 있어서 1번 옵션도 거의 쓸 일 없다.


잡힌 함정

한글 파일명 깨짐. macOS는 NFD, 윈도우/리눅스는 NFC. safe_filename에서 unicodedata.normalize('NFC', s) 추가하면 안전.

중첩된 outline 구조. PyPDF의 outline은 챕터 안의 섹션이 list로 중첩돼 들어 있다. isinstance(item, list)로 스킵하면 챕터 단위만 추출.

마지막 챕터 페이지 끝. i+1 < len(chapters)가 빠지면 IndexError. else len(reader.pages)로 처리.


한 줄 결론

PDF 챕터별 분할은 GUI 도구 깔지 말고 위 30줄로 끝낸다.

python split_pdf.py 책.pdf

도구를 만드는 데 5분, 그 후엔 30초씩 절약. ROI가 높은 자동화는 작은 데서 시작한다.