Skip to content

Instantly share code, notes, and snippets.

@partrita
Created June 16, 2025 02:22
Show Gist options
  • Save partrita/4848eb53d9b64eff4af4223625db2171 to your computer and use it in GitHub Desktop.
Save partrita/4848eb53d9b64eff4af4223625db2171 to your computer and use it in GitHub Desktop.
python script for bulk pdf conversion. even faster.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "click",
# "nbconvert[webpdf]",
# ]
# ///
import os
import subprocess
import click
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
# 이 함수는 최상위 레벨에 있어야 피클링 가능합니다.
def convert_single_ipynb_to_pdf(ipynb_filepath, output_filepath_base):
"""
단일 .ipynb 파일을 PDF로 변환하는 함수.
병렬 처리를 위해 독립적인 함수로 분리.
"""
filename = os.path.basename(ipynb_filepath)
expected_pdf_filename = f"{os.path.basename(output_filepath_base)}.pdf"
try:
# 서브 프로세스 내에서 click.echo는 stdout/stderr 충돌을 일으킬 수 있으므로
# 더 안전하게 표준 에러 스트림으로 출력하거나, 로깅 모듈을 사용하는 것이 좋습니다.
# 여기서는 click.echo(..., err=True)를 유지합니다.
click.echo(f" '{filename}' 변환 중...", err=True)
command = [
"jupyter",
"nbconvert",
"--to",
"webpdf",
"--allow-chromium-download",
ipynb_filepath,
"--output",
output_filepath_base
]
# subprocess.run의 capture_output=True는 자식 프로세스의 stdout/stderr를 캡처합니다.
# 실제 출력을 보려면 capture_output=False로 설정하거나, Popen을 직접 사용해야 할 수 있습니다.
subprocess.run(command, check=True, capture_output=True, text=True)
click.echo(f" 성공: '{filename}' -> '{expected_pdf_filename}'", err=True)
return True, filename
except subprocess.CalledProcessError as e:
click.echo(f" 오류: '{filename}' 변환 실패.", err=True)
click.echo(f" 명령어: {' '.join(e.cmd)}", err=True)
click.echo(f" 표준 오류: {e.stderr.strip()}", err=True)
return False, filename
except FileNotFoundError:
click.echo(f" 오류: 'jupyter' 명령어를 찾을 수 없습니다. Jupyter가 설치되어 있고 PATH에 추가되었는지 확인하세요.", err=True)
return False, filename
except Exception as e:
click.echo(f" 예기치 않은 오류 발생: {e}", err=True)
return False, filename
@click.command()
@click.option('--input_folder', '-i',
type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True, resolve_path=True),
required=True,
help='IPython Notebook (.ipynb) 파일이 있는 입력 폴더 경로.')
@click.option('--output_folder', '-o',
type=click.Path(file_okay=False, dir_okay=True, writable=True, resolve_path=True),
default=None,
help='변환된 PDF 파일을 저장할 출력 폴더 경로. 지정하지 않으면 입력 폴더에 저장됩니다.')
@click.option('--max_workers', '-w',
type=int,
default=os.cpu_count(), # 시스템의 CPU 코어 수만큼 기본값 설정
help='동시에 실행할 변환 프로세스(또는 스레드)의 최대 개수.')
@click.option('--use_threads', is_flag=True,
help='멀티프로세싱 대신 멀티스레딩을 사용합니다. CPU 바운드 작업에는 일반적으로 멀티프로세싱이 더 좋습니다.')
def convert_ipynb_to_pdf_cli(input_folder, output_folder, max_workers, use_threads):
"""
지정된 입력 폴더의 모든 .ipynb 파일을 PDF로 변환하여 출력 폴더에 저장합니다.
병렬 처리를 지원합니다.
"""
if output_folder is None:
output_folder = input_folder
os.makedirs(output_folder, exist_ok=True)
click.echo(f"'{input_folder}'에서 .ipynb 파일을 찾아 '{output_folder}'(으)로 PDF 변환을 시작합니다...\n")
click.echo(f"최대 {max_workers}개의 {'스레드' if use_threads else '프로세스'}를 사용하여 변환합니다.\n")
notebook_tasks = [] # (ipynb_filepath, output_filepath_base) 튜플 리스트
for filename in os.listdir(input_folder):
if filename.endswith(".ipynb"):
ipynb_filepath = os.path.join(input_folder, filename)
base_filename_without_ext = os.path.splitext(filename)[0]
output_filepath_base = os.path.join(output_folder, base_filename_without_ext)
notebook_tasks.append((ipynb_filepath, output_filepath_base))
if not notebook_tasks:
click.echo("변환할 .ipynb 파일이 없습니다.")
return
converted_count = 0
failed_count = 0
Executor = ThreadPoolExecutor if use_threads else ProcessPoolExecutor
with Executor(max_workers=max_workers) as executor:
# 각 작업을 제출하고 Future 객체를 리스트에 저장
futures = {executor.submit(convert_single_ipynb_to_pdf, ipynb_path, output_path): (ipynb_path, output_path)
for ipynb_path, output_path in notebook_tasks}
# 완료된 순서대로 결과 처리
for future in as_completed(futures):
# 원본 작업 정보를 가져옴 (여기서는 딱히 필요 없지만, 복잡한 경우 유용)
# original_ipynb_path, original_output_path = futures[future]
try:
success, filename = future.result() # 작업 결과 가져오기
if success:
converted_count += 1
else:
failed_count += 1
except Exception as exc:
# 여기서 예외가 발생하면 convert_single_ipynb_to_pdf 함수 자체에서 처리 못한 예외입니다.
click.echo(f"예외가 발생하여 작업이 완료되지 못했습니다: {exc}", err=True)
# 이 경우에도 실패로 간주할 수 있습니다.
failed_count += 1
click.echo(f"\n--- 변환 요약 ---")
click.echo(f"총 성공: {converted_count}개")
click.echo(f"총 실패: {failed_count}개")
click.echo(f"모든 .ipynb 파일 변환 시도 완료.")
if __name__ == "__main__":
# Windows에서는 if __name__ == "__main__": 블록 안에 모든 실행 코드를 넣어야
# 멀티프로세싱이 올바르게 작동합니다.
convert_ipynb_to_pdf_cli()
# 10개의 프로세스 사용 uv run convert_notebooks_cli.py -i ./my_notebooks -o ./converted_pdfs -w 10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment