import fitz import os import re def clean_and_format_text(text: str) -> str: """ 추출된 텍스트 청크를 문제/선택지 중심으로 재구성하고 모든 예외 규칙에 따라 서식을 정리합니다. 각 정규식은 특정 예외 케이스를 해결하기 위해 논리적으로 설계되었습니다. """ # 1단계: 초기 정제 - 후속 처리를 용이하게 하기 위해 텍스트를 한 줄로 정렬하고 기본 서식을 정리합니다. # 모든 줄바꿈 문자(\n)를 공백으로 변환하여, 여러 줄에 걸쳐 있던 텍스트를 다루기 쉬운 한 줄의 문자열로 만듭니다. processed_text = text.replace('\n', ' ').strip() # 단어와 공백으로 떨어진 문장부호(.,?!)를 단어에 붙입니다. (예: "단어 ." -> "단어.") # \s+는 하나 이상의 공백을, ([.,?!])는 문장부호 그룹을 의미하며, \1은 찾은 그룹(문장부호)을 가리킵니다. processed_text = re.sub(r'\s+([.,?!])', r'\1', processed_text) # 두 개 이상의 연속된 공백을 하나의 공백으로 통일하여 일관성을 유지합니다. processed_text = re.sub(r'\s{2,}', ' ', processed_text) # 2단계: 구조적 줄바꿈 삽입 - 문맥을 파악하여 문단 구분이 필요한 곳에 줄바꿈 문자를 삽입합니다. # '[숫자~숫자]...답하시오.' 형식의 장문 독해 안내문 전체를 별도의 문단으로 분리합니다. # ( )로 묶인 전체 패턴을 그룹(\1)으로 찾아, 그 앞과 뒤에 줄바꿈을 추가합니다. processed_text = re.sub(r'(\[\d{1,2}~\d{1,2}\].*?답하시오\.)', r'\n\n\1\n', processed_text) # '[3점]'과 각주 기호 '*'가 붙어있는 특정 케이스를 분리합니다. processed_text = re.sub(r'(\[\d점\])\s*(\*)', r'\1\n\2', processed_text) # 한글 또는 닫는 괄호')' 뒤에 대괄호'['가 오는 경우(예: ...상쇄하다[36~37])를 찾아 줄바꿈을 추가합니다. processed_text = re.sub(r'([가-힣\)])\s*(\[)', r'\1\n\2', processed_text) # 일반적인 각주 분리 규칙. 단어의 끝으로 간주되는 문자들 뒤에 각주 기호 '*'가 오면 줄바꿈을 삽입합니다. # [a-zA-Z가-힣0-9.,?!")’\']는 단어의 끝을 구성하는 다양한 문자셋을 의미합니다. processed_text = re.sub(r'([a-zA-Z가-힣0-9.,?!")’\'])\s+(\*)', r'\1\n\2', processed_text) # '고르시오.', '것은?', '문장은?'으로 끝나는 문제 제목과 본문을 분리합니다. # (?:...)는 캡처하지 않는 그룹을 의미하며, 제목 뒤에 선택적으로 오는 '[3점]'까지 하나의 단위로 묶어 처리합니다. processed_text = re.sub(r'((?:고르시오\.|것은\?|문장은\?)(?:\s*\[\d점\])?)\s*', r'\1\n', processed_text) # 요약문 기호 '󰀻'를 찾아, 그 앞뒤로 충분한 간격을 주어 별도의 문단처럼 보이게 합니다. processed_text = re.sub(r'\s*(󰀻)\s*', r'\n\n\1\n\n', processed_text) # 각주와 선택지(①)가 한 줄에 붙어있는 경우를 분리합니다. (2차 방어 로직) processed_text = re.sub(r'(\*.+?\w)\s*(①|②|③|④|⑤)', r'\1\n\2', processed_text) # 원문자 뒤에 공백이 없는 경우(예: ①단어) 공백을 강제로 추가합니다. # (?!\s)는 '부정형 전방탐색'으로, 뒤에 공백이 오지 않는 경우에만 패턴을 일치시킵니다. processed_text = re.sub(r'(①|②|③|④|⑤)(?!\s)', r'\1 ', processed_text) # 문제 번호를 인식하여 그 앞에 두 번 줄바꿈을 삽입합니다. # (? str: # (이전과 동일) try: doc = fitz.open(input_pdf_path) except Exception as e: print(f"오류: PDF 파일을 열 수 없습니다. 경로: {input_pdf_path}, 오류: {e}") return "" all_text_parts = [] MM_TO_POINTS = 72 / 25.4 page_margins = { 0: {'top': 75 * MM_TO_POINTS, 'bottom': 40 * MM_TO_POINTS}, 'default': {'top': 54 * MM_TO_POINTS, 'bottom': 40 * MM_TO_POINTS} } print(f"'{os.path.basename(input_pdf_path)}' 파일 ({len(doc)}페이지) 텍스트 추출 중...") for page_num in range(len(doc)): page = doc.load_page(page_num) page_rect = page.rect page_width, page_height = page_rect.width, page_rect.height margins = page_margins.get(page_num, page_margins['default']) top_margin, bottom_margin = margins['top'], margins['bottom'] mid_x = page_width / 2 left_rect = fitz.Rect(0, top_margin, mid_x, page_height - bottom_margin) right_rect = fitz.Rect(mid_x, top_margin, page_width, page_height - bottom_margin) for rect_area in [left_rect, right_rect]: raw_text = page.get_text("text", clip=rect_area) if raw_text.strip(): formatted_text = clean_and_format_text(raw_text) all_text_parts.append(formatted_text) # 최종적으로 합치기 전에, 각 파트의 앞뒤 공백을 다시 한번 정리하여 일관성을 높임 cleaned_parts = [part.strip() for part in all_text_parts if part.strip()] return '\n\n'.join(cleaned_parts) if __name__ == '__main__': base_folder = r'D:\영어 문제지' output_folder = os.path.join(base_folder, 'txt') if not os.path.exists(output_folder): os.makedirs(output_folder) try: all_files = os.listdir(base_folder) except FileNotFoundError: print(f"오류: '{base_folder}' 폴더를 찾을 수 없습니다. 경로를 확인하세요.") exit() pdf_files = [f for f in all_files if f.lower().endswith('.pdf')] if not pdf_files: print(f"'{base_folder}' 폴더에 처리할 PDF 파일이 없습니다.") else: print(f"총 {len(pdf_files)}개의 PDF 파일을 처리합니다.") for pdf_filename in pdf_files: input_pdf_path = os.path.join(base_folder, pdf_filename) combined_text = extract_combined_text_from_pdf(input_pdf_path) if combined_text: # === 최종 후처리 단계 (전체 텍스트 대상) === # 1. 불필요한 안내 문구 목록 lines_to_remove = [ "1번부터 17번까지는 듣고 답하는 문제입니다. 1번부터 15번까지는 한 번만 들려주고, 16번부터 17번까지는 두 번 들려줍니다. 방송을 잘 듣고 답을 하시기 바랍니다.", "이제 듣기 문제가 끝났습니다. 18번부터는 문제지의 지시에 따라 답을 하시기 바랍니다.", "* 확인 사항 ◦답안지의 해당란에 필요한 내용을 정확히 기입(표기)했는지 확인 하시오." ] for line in lines_to_remove: combined_text = combined_text.replace(line, "") # 2. 탭(tab)으로 정렬할 특정 선택지 블록 처리 def tab_format_choices(match): # 찾은 블록의 줄바꿈을 탭으로 변경 return match.group(0).replace('\n', '\t') pattern = r'(?:^[①②③④⑤]\s*\([a-e]\)\s*\n){4}^[①②③④⑤]\s*\([a-e]\)$' combined_text = re.sub(pattern, tab_format_choices, combined_text, flags=re.MULTILINE) # 3. 위의 처리 과정에서 생길 수 있는 과도한 줄바꿈 정리 combined_text = re.sub(r'\n{3,}', '\n\n', combined_text).strip() # 최종 결과 저장 base_filename = os.path.splitext(pdf_filename)[0] output_filename = f"{base_filename}.txt" output_filepath = os.path.join(output_folder, output_filename) with open(output_filepath, 'w', encoding='utf-8') as f: f.write(combined_text) print(f"-> 최종 결과가 '{output_filepath}' 파일에 저장되었습니다.") print("\n모든 작업이 완료되었습니다.")