feat: MD 파일 병합 및 이미지 경로 통합 스크립트 추가 (#1)
- 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>
This commit is contained in:
327
main.py
Normal file
327
main.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/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:])
|
||||
Reference in New Issue
Block a user