- merge_markdown.py: 96개 페이지별 MD를 단일 파일로 병합
- 이미지를 output/images/ 폴더로 통합, p{NN}_ prefix로 파일명 충돌 방지
- file_range 파라미터로 부분 테스트 가능
- docs/tutorial.md: merge 명령어 및 사용법 문서화
- docs/history: 작업 이력 파일 추가
소요 시간: 10분 | Context: input 18k / output 2k tokens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
13 KiB
Python
328 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
docuConverter — 문서 → Markdown 변환 도구 모음
|
|
|
|
지원 포맷:
|
|
PDF → Markdown (marker-pdf 기반, 이미지 유/무 선택)
|
|
EPUB → Markdown (ebooklib + BeautifulSoup 기반)
|
|
|
|
시나리오:
|
|
1. PDF 단일 변환 (이미지 포함, 고품질)
|
|
2. PDF 단일 변환 (텍스트 전용, 빠름)
|
|
3. PDF 배치 변환 (이미지 포함, 순차)
|
|
4. PDF 배치 변환 (텍스트 전용, 순차, 빠름)
|
|
5. PDF 배치 변환 (병렬 처리, 멀티코어)
|
|
6. EPUB 단일 변환
|
|
7. EPUB 배치 변환
|
|
8. 이미지만 추출 (PDF → 이미지 파일)
|
|
9. Markdown 병합 (output/ 폴더의 .md 파일들을 하나로)
|
|
10. 이미지 경로 업데이트 (Markdown 내 이미지 링크 재연결)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import glob
|
|
from pathlib import Path
|
|
|
|
|
|
# ─── 시나리오 함수들 ──────────────────────────────────────────────────────────
|
|
|
|
def scenario_pdf_single_with_images():
|
|
"""PDF 단일 변환 — 이미지 포함 (고품질, 느림)"""
|
|
from convert_with_cropped_images import convert_pdf_with_cropped_images
|
|
|
|
pdf_path = input("변환할 PDF 경로를 입력하세요: ").strip()
|
|
if not pdf_path:
|
|
pdf_files = sorted(glob.glob("input/*.pdf"))
|
|
if not pdf_files:
|
|
print("ERROR: input/ 폴더에 PDF 파일이 없습니다.")
|
|
return
|
|
pdf_path = pdf_files[0]
|
|
print(f" → 자동 선택: {pdf_path}")
|
|
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
convert_pdf_with_cropped_images(pdf_path, output_dir)
|
|
|
|
|
|
def scenario_pdf_single_fast():
|
|
"""PDF 단일 변환 — 텍스트 전용 (빠름)"""
|
|
from convert_pdfs_fast import convert_pdf_to_markdown_fast
|
|
|
|
pdf_path = input("변환할 PDF 경로를 입력하세요: ").strip()
|
|
if not pdf_path:
|
|
pdf_files = sorted(glob.glob("input/*.pdf"))
|
|
if not pdf_files:
|
|
print("ERROR: input/ 폴더에 PDF 파일이 없습니다.")
|
|
return
|
|
pdf_path = pdf_files[0]
|
|
print(f" → 자동 선택: {pdf_path}")
|
|
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
convert_pdf_to_markdown_fast(pdf_path, output_dir)
|
|
|
|
|
|
def scenario_pdf_batch_with_images():
|
|
"""PDF 배치 변환 — 이미지 포함 (순차, input/ → output/)"""
|
|
from convert_with_cropped_images import convert_all_pdfs
|
|
|
|
input_dir = input("입력 폴더 [기본: input]: ").strip() or "input"
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
convert_all_pdfs(input_dir, output_dir)
|
|
|
|
|
|
def scenario_pdf_batch_fast():
|
|
"""PDF 배치 변환 — 텍스트 전용 (순차, 빠름)"""
|
|
from convert_pdfs_fast import convert_all_pdfs_fast
|
|
|
|
input_dir = input("입력 폴더 [기본: input]: ").strip() or "input"
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
convert_all_pdfs_fast(input_dir, output_dir)
|
|
|
|
|
|
def scenario_pdf_batch_parallel():
|
|
"""PDF 배치 변환 — 병렬 처리 (멀티코어)"""
|
|
from convert_pdfs_parallel import convert_all_pdfs_parallel
|
|
import multiprocessing
|
|
|
|
input_dir = input("입력 폴더 [기본: input]: ").strip() or "input"
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
cpu_count = multiprocessing.cpu_count()
|
|
workers_input = input(f"병렬 워커 수 [기본: 2, CPU: {cpu_count}]: ").strip()
|
|
max_workers = int(workers_input) if workers_input.isdigit() else 2
|
|
convert_all_pdfs_parallel(input_dir, output_dir, max_workers)
|
|
|
|
|
|
def scenario_epub_single():
|
|
"""EPUB 단일 변환 → Markdown"""
|
|
from convert_epub import convert_epub_to_markdown
|
|
|
|
epub_path = input("변환할 EPUB 경로를 입력하세요: ").strip()
|
|
if not epub_path:
|
|
epub_files = sorted(glob.glob("input/*.epub"))
|
|
if not epub_files:
|
|
print("ERROR: input/ 폴더에 EPUB 파일이 없습니다.")
|
|
return
|
|
epub_path = epub_files[0]
|
|
print(f" → 자동 선택: {epub_path}")
|
|
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
convert_epub_to_markdown(epub_path, output_dir)
|
|
|
|
|
|
def scenario_epub_batch():
|
|
"""EPUB 배치 변환 — input/ 폴더의 모든 .epub 파일"""
|
|
from convert_epub import convert_epub_to_markdown
|
|
|
|
input_dir = input("입력 폴더 [기본: input]: ").strip() or "input"
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
|
|
epub_files = sorted(glob.glob(os.path.join(input_dir, "*.epub")))
|
|
if not epub_files:
|
|
print(f"ERROR: {input_dir}/ 폴더에 EPUB 파일이 없습니다.")
|
|
return
|
|
|
|
print(f"Found {len(epub_files)} EPUB file(s)")
|
|
print("=" * 60)
|
|
successful = 0
|
|
failed = 0
|
|
for i, epub_file in enumerate(epub_files, 1):
|
|
print(f"\n[{i}/{len(epub_files)}] {Path(epub_file).name}")
|
|
try:
|
|
convert_epub_to_markdown(epub_file, output_dir)
|
|
successful += 1
|
|
except Exception as e:
|
|
print(f" ERROR: {e}")
|
|
failed += 1
|
|
|
|
print("\n" + "=" * 60)
|
|
print(f"Conversion complete! Successful: {successful}, Failed: {failed}")
|
|
|
|
|
|
def scenario_extract_images():
|
|
"""PDF에서 이미지만 추출 (Markdown 변환 없음)"""
|
|
from extract_images import extract_all_images, extract_images_from_pdf
|
|
|
|
mode = input("모드 선택 — [1] 단일 파일 [2] 배치 (input/ 폴더): ").strip()
|
|
output_dir = input("출력 폴더 [기본: output]: ").strip() or "output"
|
|
|
|
if mode == "1":
|
|
pdf_path = input("PDF 경로를 입력하세요: ").strip()
|
|
if not pdf_path:
|
|
print("ERROR: 경로가 비어 있습니다.")
|
|
return
|
|
extract_images_from_pdf(pdf_path, output_dir)
|
|
else:
|
|
input_dir = input("입력 폴더 [기본: input]: ").strip() or "input"
|
|
extract_all_images(input_dir, output_dir)
|
|
|
|
|
|
def scenario_merge_markdown():
|
|
"""output/ 폴더의 .md 파일들을 하나의 파일로 병합"""
|
|
from merge_markdown import merge_markdown_files
|
|
|
|
input_dir = input("병합할 Markdown 폴더 [기본: output]: ").strip() or "output"
|
|
output_file = input("병합 결과 파일명 [기본: merged_all.md]: ").strip() or "merged_all.md"
|
|
separator_choice = input("구분자 — [1] 수평선 (---) [2] 빈줄만: ").strip()
|
|
separator = "\n\n---\n\n" if separator_choice != "2" else "\n\n"
|
|
merge_markdown_files(input_dir, output_file, separator)
|
|
|
|
|
|
def scenario_update_image_paths():
|
|
"""Markdown 내 이미지 경로를 추출된 실제 이미지 경로로 업데이트"""
|
|
from update_image_paths import update_all_markdown_files
|
|
|
|
output_dir = input("Markdown 폴더 [기본: output]: ").strip() or "output"
|
|
update_all_markdown_files(output_dir)
|
|
|
|
|
|
# ─── 메뉴 ────────────────────────────────────────────────────────────────────
|
|
|
|
SCENARIOS = [
|
|
("PDF 단일 변환 (이미지 포함, 고품질)", scenario_pdf_single_with_images),
|
|
("PDF 단일 변환 (텍스트 전용, 빠름)", scenario_pdf_single_fast),
|
|
("PDF 배치 변환 (이미지 포함, 순차)", scenario_pdf_batch_with_images),
|
|
("PDF 배치 변환 (텍스트 전용, 순차, 빠름)", scenario_pdf_batch_fast),
|
|
("PDF 배치 변환 (병렬 처리, 멀티코어)", scenario_pdf_batch_parallel),
|
|
("EPUB 단일 변환 → Markdown", scenario_epub_single),
|
|
("EPUB 배치 변환 (input/ 폴더 전체)", scenario_epub_batch),
|
|
("이미지만 추출 (PDF → 이미지 파일)", scenario_extract_images),
|
|
("Markdown 파일 병합 (여러 .md → 하나로)", scenario_merge_markdown),
|
|
("이미지 경로 업데이트 (Markdown 링크 수정)", scenario_update_image_paths),
|
|
]
|
|
|
|
|
|
def print_menu():
|
|
print("\n" + "=" * 60)
|
|
print(" docuConverter — 문서 → Markdown 변환 도구")
|
|
print("=" * 60)
|
|
for i, (label, _) in enumerate(SCENARIOS, 1):
|
|
print(f" {i:2}. {label}")
|
|
print(" 0. 종료")
|
|
print("=" * 60)
|
|
|
|
|
|
def run_interactive():
|
|
"""대화형 메뉴 실행"""
|
|
while True:
|
|
print_menu()
|
|
choice = input("시나리오 번호를 선택하세요: ").strip()
|
|
|
|
if choice == "0":
|
|
print("종료합니다.")
|
|
break
|
|
|
|
if not choice.isdigit() or not (1 <= int(choice) <= len(SCENARIOS)):
|
|
print("잘못된 입력입니다. 다시 선택하세요.")
|
|
continue
|
|
|
|
idx = int(choice) - 1
|
|
label, fn = SCENARIOS[idx]
|
|
print(f"\n▶ {label}")
|
|
print("-" * 60)
|
|
try:
|
|
fn()
|
|
except KeyboardInterrupt:
|
|
print("\n중단되었습니다.")
|
|
except Exception as e:
|
|
print(f"\nERROR: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
input("\n[Enter] 키를 누르면 메뉴로 돌아갑니다...")
|
|
|
|
|
|
def run_cli(args):
|
|
"""CLI 직접 실행 모드 (비대화형)
|
|
|
|
사용 예:
|
|
python main.py 1 path/to/file.pdf output/
|
|
python main.py 4 input/ output/
|
|
python main.py 6 path/to/book.epub output/
|
|
"""
|
|
if not args:
|
|
run_interactive()
|
|
return
|
|
|
|
scenario_num = args[0]
|
|
if not scenario_num.isdigit() or not (1 <= int(scenario_num) <= len(SCENARIOS)):
|
|
print(f"ERROR: 시나리오 번호는 1~{len(SCENARIOS)} 사이여야 합니다.")
|
|
sys.exit(1)
|
|
|
|
idx = int(scenario_num) - 1
|
|
label, fn = SCENARIOS[idx]
|
|
print(f"▶ {label}")
|
|
|
|
# 인자를 stdin 처럼 흉내 내어 input() 호출을 우회
|
|
# 직접 함수를 시나리오별로 호출
|
|
extra = args[1:]
|
|
|
|
if idx == 0: # PDF 단일, 이미지 포함
|
|
from convert_with_cropped_images import convert_pdf_with_cropped_images
|
|
pdf_path = extra[0] if len(extra) > 0 else sorted(glob.glob("input/*.pdf"))[0]
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
convert_pdf_with_cropped_images(pdf_path, out)
|
|
|
|
elif idx == 1: # PDF 단일, fast
|
|
from convert_pdfs_fast import convert_pdf_to_markdown_fast
|
|
pdf_path = extra[0] if len(extra) > 0 else sorted(glob.glob("input/*.pdf"))[0]
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
convert_pdf_to_markdown_fast(pdf_path, out)
|
|
|
|
elif idx == 2: # PDF 배치, 이미지 포함
|
|
from convert_with_cropped_images import convert_all_pdfs
|
|
inp = extra[0] if len(extra) > 0 else "input"
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
convert_all_pdfs(inp, out)
|
|
|
|
elif idx == 3: # PDF 배치, fast
|
|
from convert_pdfs_fast import convert_all_pdfs_fast
|
|
inp = extra[0] if len(extra) > 0 else "input"
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
convert_all_pdfs_fast(inp, out)
|
|
|
|
elif idx == 4: # PDF 배치, 병렬
|
|
from convert_pdfs_parallel import convert_all_pdfs_parallel
|
|
inp = extra[0] if len(extra) > 0 else "input"
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
workers = int(extra[2]) if len(extra) > 2 else 2
|
|
convert_all_pdfs_parallel(inp, out, workers)
|
|
|
|
elif idx == 5: # EPUB 단일
|
|
from convert_epub import convert_epub_to_markdown
|
|
epub_path = extra[0] if len(extra) > 0 else sorted(glob.glob("input/*.epub"))[0]
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
convert_epub_to_markdown(epub_path, out)
|
|
|
|
elif idx == 6: # EPUB 배치
|
|
from convert_epub import convert_epub_to_markdown
|
|
inp = extra[0] if len(extra) > 0 else "input"
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
for ep in sorted(glob.glob(os.path.join(inp, "*.epub"))):
|
|
print(f"\n→ {Path(ep).name}")
|
|
convert_epub_to_markdown(ep, out)
|
|
|
|
elif idx == 7: # 이미지 추출
|
|
from extract_images import extract_all_images
|
|
inp = extra[0] if len(extra) > 0 else "input"
|
|
out = extra[1] if len(extra) > 1 else "output"
|
|
extract_all_images(inp, out)
|
|
|
|
elif idx == 8: # Markdown 병합
|
|
from merge_markdown import merge_markdown_files
|
|
inp = extra[0] if len(extra) > 0 else "output"
|
|
out_file = extra[1] if len(extra) > 1 else "merged_all.md"
|
|
merge_markdown_files(inp, out_file)
|
|
|
|
elif idx == 9: # 이미지 경로 업데이트
|
|
from update_image_paths import update_all_markdown_files
|
|
out = extra[0] if len(extra) > 0 else "output"
|
|
update_all_markdown_files(out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# docuConverter 폴더를 cwd로 설정 (어느 경로에서 실행해도 input/output 경로 일관)
|
|
script_dir = Path(__file__).parent
|
|
os.chdir(script_dir)
|
|
|
|
run_cli(sys.argv[1:])
|