#!/usr/bin/env python3 """ ISO21500 PDF를 마크다운 파일로 변환하는 스크립트 PMBOK_4th_Edition_md 폴더의 형식을 따름 """ import os import re import fitz # PyMuPDF from pathlib import Path # 경로 설정 BASE_DIR = Path(__file__).parent.parent PDF_DIR = BASE_DIR / "src/model/resources/pmbokpdf/extracted-iso21500" OUTPUT_DIR = BASE_DIR / "src/model/resources/pmbokpdf/ISO21500_md" def extract_text_from_pdf(pdf_path: Path) -> str: """PDF에서 텍스트 추출""" doc = fitz.open(pdf_path) text = "" for page in doc: text += page.get_text() doc.close() return text.strip() def extract_keywords(text: str) -> list: """텍스트에서 키워드 추출 (간단한 방식)""" # ISO 21500 관련 주요 키워드 keyword_patterns = [ r"프로젝트관리", r"프로젝트경영", r"ISO\s*21500", r"PMBOK", r"프로세스\s*그룹", r"지식영역", r"착수", r"기획", r"실행", r"통제", r"종료", r"통합관리", r"이해관계자", r"범위관리", r"자원관리", r"일정관리", r"원가관리", r"리스크관리", r"품질관리", r"조달관리", r"의사소통", r"입력물", r"출력물", r"산출물", r"도구", r"기법", r"WBS", r"작업분류체계", r"간트", r"네트워크", r"일정표", r"예산", r"비용", r"자원", r"인력", r"팀", r"스폰서", r"프로젝트관리자", r"PM", r"PMO", r"생애주기", r"단계", r"마일스톤", r"인도물", ] found_keywords = [] for pattern in keyword_patterns: if re.search(pattern, text, re.IGNORECASE): # 패턴에서 정규식 메타문자 제거 clean_keyword = pattern.replace(r"\s*", " ").replace("\\", "") if clean_keyword not in found_keywords: found_keywords.append(clean_keyword) return found_keywords[:10] # 최대 10개 키워드 def detect_chapter_info(text: str, page_num: int) -> dict: """텍스트에서 챕터 정보 추출""" chapter_patterns = [ (r"제\s*(\d+)\s*장[:\s]*(.+?)(?:\n|$)", "chapter"), (r"(\d+)\.\s*(.+?)(?:\n|$)", "section"), (r"부록\s*([A-Z가-힣]+)[:\s]*(.+?)(?:\n|$)", "appendix"), ] chapter = None chapter_title = None section = None section_title = None for pattern, ptype in chapter_patterns: match = re.search(pattern, text[:500]) if match: if ptype == "chapter": chapter = match.group(1) chapter_title = match.group(2).strip() elif ptype == "section": section = match.group(1) section_title = match.group(2).strip() elif ptype == "appendix": chapter = f"부록 {match.group(1)}" chapter_title = match.group(2).strip() return { "chapter": chapter, "chapter_title": chapter_title, "section": section, "section_title": section_title } def detect_process_group(text: str) -> str: """프로세스 그룹 감지""" process_groups = { "착수": ["착수", "Initiating", "시작"], "기획": ["기획", "Planning", "계획"], "실행": ["실행", "Executing", "수행"], "통제": ["통제", "Controlling", "감시", "모니터링"], "종료": ["종료", "Closing", "마무리"] } for group, keywords in process_groups.items(): for keyword in keywords: if keyword in text[:1000]: return group return None def detect_knowledge_area(text: str) -> str: """지식영역 감지""" knowledge_areas = { "통합관리": ["통합관리", "통합", "Integration"], "이해관계자관리": ["이해관계자", "Stakeholder"], "범위관리": ["범위관리", "범위", "Scope"], "자원관리": ["자원관리", "자원", "Resource", "인적자원"], "일정관리": ["일정관리", "일정", "Time", "Schedule"], "원가관리": ["원가관리", "원가", "비용", "Cost"], "리스크관리": ["리스크", "위험", "Risk"], "품질관리": ["품질관리", "품질", "Quality"], "조달관리": ["조달관리", "조달", "Procurement"], "의사소통관리": ["의사소통", "커뮤니케이션", "Communication"] } for area, keywords in knowledge_areas.items(): for keyword in keywords: if keyword in text[:1000]: return area return None def text_to_markdown(text: str) -> str: """텍스트를 마크다운 형식으로 변환""" lines = text.split('\n') md_lines = [] for line in lines: line = line.strip() if not line: md_lines.append("") continue # 제목 패턴 감지 if re.match(r'^제\s*\d+\s*장', line): md_lines.append(f"# {line}") elif re.match(r'^\d+\.\d+\.\d+', line): md_lines.append(f"### {line}") elif re.match(r'^\d+\.\d+', line): md_lines.append(f"## {line}") elif re.match(r'^[가-힣]+\s*\d+', line) or re.match(r'^표\s*\d+', line) or re.match(r'^그림\s*\d+', line): md_lines.append(f"**{line}**") elif line.startswith('✣') or line.startswith('■') or line.startswith('●'): md_lines.append(f"- {line[1:].strip()}") elif line.startswith('-') or line.startswith('•'): md_lines.append(f"- {line[1:].strip()}") else: md_lines.append(line) return '\n'.join(md_lines) def create_md_file(pdf_path: Path, output_dir: Path): """PDF 파일을 마크다운 파일로 변환""" # 파일명에서 페이지 번호 추출 filename = pdf_path.stem # ISO21500-11 형식 match = re.search(r'ISO21500-(\d+)', filename) if not match: print(f"Skip: {filename} (invalid format)") return page_num = int(match.group(1)) # 텍스트 추출 text = extract_text_from_pdf(pdf_path) if not text or len(text) < 50: print(f"Skip: {filename} (empty or too short)") return # 메타데이터 추출 chapter_info = detect_chapter_info(text, page_num) keywords = extract_keywords(text) process_group = detect_process_group(text) knowledge_area = detect_knowledge_area(text) # 마크다운 본문 생성 md_content = text_to_markdown(text) # 벡터 DB 검색용 요약 생성 summary_text = text[:500].replace('\n', ' ').strip() if len(text) > 500: summary_text += "..." # YAML 프론트매터 생성 yaml_front = f'''--- source: ISO 21500 이행가이드 file: {pdf_path.name} page_number: {page_num} chapter: {chapter_info.get('chapter') or 'null'} chapter_title: {chapter_info.get('chapter_title') or 'null'} section: "{chapter_info.get('section') or ''}" section_title: {chapter_info.get('section_title') or 'null'} process_group: {process_group or 'null'} knowledge_area: {knowledge_area or 'null'} keywords: {keywords} --- ''' # 최종 마크다운 파일 내용 full_content = yaml_front + md_content + f''' --- ## 벡터 DB 검색용 전문 {summary_text} ''' # 파일 저장 output_path = output_dir / f"page_{page_num:03d}.md" output_path.write_text(full_content, encoding='utf-8') print(f"Created: {output_path.name}") def main(): """메인 함수""" print("=" * 60) print("ISO21500 PDF to Markdown Converter") print("=" * 60) # 출력 디렉토리 생성 OUTPUT_DIR.mkdir(parents=True, exist_ok=True) # PDF 파일 목록 가져오기 pdf_files = sorted(PDF_DIR.glob("ISO21500-*.pdf"), key=lambda x: int(re.search(r'(\d+)', x.stem).group(1))) print(f"Found {len(pdf_files)} PDF files") print(f"Output directory: {OUTPUT_DIR}") print("-" * 60) # 각 PDF 파일 변환 success_count = 0 for pdf_path in pdf_files: try: create_md_file(pdf_path, OUTPUT_DIR) success_count += 1 except Exception as e: print(f"Error: {pdf_path.name} - {e}") print("-" * 60) print(f"Conversion complete: {success_count}/{len(pdf_files)} files") if __name__ == "__main__": main()