import json import os from typing import Any import pdfplumber from langchain.schema import HumanMessage, SystemMessage from langchain_core.embeddings import Embeddings from langchain_core.language_models import BaseChatModel try: from docx import Document except ImportError: Document = None try: import docx2txt except ImportError: docx2txt = None class EmbeddingsModel: def __init__(self, model: Embeddings): self.model = model def get_model(self): return self.model class ChatModel: def __init__(self, model: BaseChatModel): self.model = model def get_llm(self): return self.model class ResumeParser: def __init__(self, chat_model: ChatModel): self.llm = chat_model.get_llm() self.resume_prompt = """ Проанализируй текст резюме и извлеки из него структурированные данные в JSON формате. Верни только JSON без дополнительных комментариев. Формат ответа: {{ "name": "Имя кандидата", "email": "email@example.com", "phone": "+7-XXX-XXX-XX-XX", "skills": ["навык1", "навык2", "навык3"], "experience": [ {{ "company": "Название компании", "position": "Должность", "period": "2021-2024", "description": "Краткое описание обязанностей" }} ], "total_years": 3.5, "education": "Образование", "summary": "Краткое резюме о кандидате" }} Текст резюме: {resume_text} """ def extract_text_from_pdf(self, file_path: str) -> str: """Извлекает текст из PDF файла""" try: with pdfplumber.open(file_path) as pdf: text = "\n".join([page.extract_text() or "" for page in pdf.pages]) return text.strip() except Exception as e: raise Exception(f"Ошибка при чтении PDF: {str(e)}") from e def extract_text_from_docx(self, file_path: str) -> str: """Извлекает текст из DOCX файла""" try: print(f"[DEBUG] Extracting DOCX text from: {file_path}") if docx2txt: # Предпочитаем docx2txt для простого извлечения текста print("[DEBUG] Using docx2txt") text = docx2txt.process(file_path) if text: print(f"[DEBUG] Extracted {len(text)} characters using docx2txt") return text.strip() else: print("[DEBUG] docx2txt returned empty text") if Document: # Используем python-docx как fallback print("[DEBUG] Using python-docx as fallback") doc = Document(file_path) text = "\n".join([paragraph.text for paragraph in doc.paragraphs]) print(f"[DEBUG] Extracted {len(text)} characters using python-docx") return text.strip() raise Exception( "Библиотеки для чтения DOCX не установлены (docx2txt или python-docx)" ) except Exception as e: print(f"[DEBUG] DOCX extraction failed: {str(e)}") raise Exception(f"Ошибка при чтении DOCX: {str(e)}") from e def extract_text_from_doc(self, file_path: str) -> str: """Извлекает текст из DOC файла""" try: # Метод 1: COM автоматизация Word (самый надежный для Windows) import os if os.name == "nt": # Windows try: import comtypes.client print(f"[DEBUG] Trying Word COM automation for {file_path}") word = comtypes.client.CreateObject("Word.Application") word.Visible = False doc = word.Documents.Open(file_path) text = doc.Content.Text doc.Close() word.Quit() if text and text.strip(): print( f"[DEBUG] Word COM successfully extracted {len(text)} characters" ) return text.strip() except Exception as e: print(f"[DEBUG] Word COM failed: {e}") # Метод 2: Для .doc файлов используем python-docx if Document: try: doc = Document(file_path) text = "\n".join([paragraph.text for paragraph in doc.paragraphs]) return text.strip() except Exception: # Если python-docx не может прочитать .doc, пытаемся использовать системные утилиты pass # Попытка использовать textract (универсальная библиотека для извлечения текста) try: import textract print(f"[DEBUG] Using textract to process {file_path}") text = textract.process(file_path).decode("utf-8") if text and text.strip(): print( f"[DEBUG] textract successfully extracted {len(text)} characters" ) return text.strip() else: print("[DEBUG] textract returned empty text") except ImportError as e: print(f"[DEBUG] textract not available: {e}") except Exception as e: print(f"[DEBUG] textract failed: {e}") # Попытка использовать docx2txt try: import docx2txt text = docx2txt.process(file_path) if text: return text.strip() except ImportError: pass except Exception: pass # Попытка использовать oletools для старых DOC файлов try: from oletools import olefile from oletools.olevba import VBA_Parser if olefile.isOleFile(file_path): # Это старый формат DOC, пытаемся извлечь текст # Пока что возвращаем информативную ошибку pass except ImportError: pass except Exception: pass raise Exception( "Не удалось извлечь текст из DOC файла ни одним из методов. " "Возможные причины:\n" "1. Microsoft Word не установлен (для COM автоматизации)\n" "2. Файл поврежден или не содержит текста\n" "3. Файл защищен паролем\n" "Рекомендуется конвертировать DOC в DOCX формат." ) except Exception as e: raise Exception(f"Ошибка при чтении DOC: {str(e)}") from e def extract_text_from_txt(self, file_path: str) -> str: """Извлекает текст из TXT файла""" try: # Попробуем разные кодировки encodings = ["utf-8", "cp1251", "latin-1", "cp1252"] for encoding in encodings: try: with open(file_path, encoding=encoding) as file: text = file.read() return text.strip() except UnicodeDecodeError: continue raise Exception("Не удалось определить кодировку текстового файла") except Exception as e: raise Exception(f"Ошибка при чтении TXT: {str(e)}") from e def extract_text_from_file(self, file_path: str) -> str: """Универсальный метод извлечения текста из файла""" if not os.path.exists(file_path): raise Exception(f"Файл не найден: {file_path}") # Определяем расширение файла _, ext = os.path.splitext(file_path.lower()) # Добавляем отладочную информацию print(f"[DEBUG] Parsing file: {file_path}, detected extension: {ext}") if ext == ".pdf": return self.extract_text_from_pdf(file_path) elif ext == ".docx": return self.extract_text_from_docx(file_path) elif ext == ".doc": return self.extract_text_from_doc(file_path) elif ext == ".txt": return self.extract_text_from_txt(file_path) else: raise Exception( f"Неподдерживаемый формат файла: {ext}. Поддерживаемые форматы: PDF, DOCX, DOC, TXT" ) def parse_resume_text(self, resume_text: str) -> dict[str, Any]: """Парсит текст резюме через LLM""" try: messages = [ SystemMessage( content="Ты эксперт по анализу резюме. Извлекай данные точно в указанном JSON формате." ), HumanMessage( content=self.resume_prompt.format(resume_text=resume_text) ), ] response = self.llm.invoke(messages) # Извлекаем JSON из ответа response_text = response.content.strip() # Пытаемся найти JSON в ответе if response_text.startswith("{") and response_text.endswith("}"): return json.loads(response_text) else: # Ищем JSON внутри текста start = response_text.find("{") end = response_text.rfind("}") + 1 if start != -1 and end > start: json_str = response_text[start:end] return json.loads(json_str) else: raise ValueError("JSON не найден в ответе LLM") except json.JSONDecodeError as e: raise Exception(f"Ошибка парсинга JSON из ответа LLM: {str(e)}") from e except Exception as e: raise Exception(f"Ошибка при обращении к LLM: {str(e)}") from e def parse_resume_from_file(self, file_path: str) -> dict[str, Any]: """Полный цикл парсинга резюме из файла""" # Шаг 1: Извлекаем текст из файла (поддерживаем PDF, DOCX, DOC, TXT) resume_text = self.extract_text_from_file(file_path) if not resume_text: raise Exception("Не удалось извлечь текст из файла") # Шаг 2: Парсим через LLM return self.parse_resume_text(resume_text)