hh-bot/hh_bot/core/job_application_manager.py

307 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from typing import List, Dict, Optional
import time
from ..config.settings import settings, AppConstants, UIFormatter
from ..config.logging_config import LoggingConfigurator
from ..models.vacancy import Vacancy, ApplicationResult
from ..services.hh_api_service import HHApiService
from ..services.gemini_service import GeminiAIService
from ..services.browser_service import BrowserService
logger = logging.getLogger(__name__)
class AutomationOrchestrator:
"""Оркестратор процесса автоматизации"""
def __init__(self):
self.api_service = HHApiService()
self.browser_service = BrowserService()
self.ai_service = None
def _get_ai_service(self):
if self.ai_service is None:
self.ai_service = GeminiAIService()
return self.ai_service
def execute_automation_pipeline(
self, keywords: Optional[str] = None, use_ai: bool = True
) -> Dict:
"""Выполнение полного пайплайна автоматизации"""
try:
vacancies = self._search_and_filter_vacancies(keywords)
if not vacancies:
return {"error": "Подходящие вакансии не найдены"}
if use_ai and self._get_ai_service().is_available():
vacancies = self._ai_filter_vacancies(vacancies)
if not vacancies:
return {"error": "После AI фильтрации подходящих вакансий нет"}
init_ok = self._initialize_browser_and_auth()
if not init_ok:
return {"error": "Ошибка инициализации браузера или авторизации"}
application_results = self._apply_to_vacancies(vacancies)
return self._create_stats(application_results)
except KeyboardInterrupt:
logger.info("Процесс остановлен пользователем")
return {"error": "Остановлено пользователем"}
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
return {"error": str(e)}
finally:
self._cleanup()
def _search_and_filter_vacancies(
self, keywords: Optional[str] = None
) -> List[Vacancy]:
"""Поиск и базовая фильтрация вакансий"""
logger.info("🔍 ЭТАП 1: Поиск вакансий")
try:
all_vacancies = self.api_service.search_vacancies(keywords)
if not all_vacancies:
logger.warning("Вакансии не найдены через API")
return []
suitable_vacancies = self.api_service.filter_suitable_vacancies(
all_vacancies, search_keywords=keywords or ""
)
self._log_search_results(all_vacancies, suitable_vacancies)
return suitable_vacancies
except Exception as e:
logger.error(f"Ошибка поиска вакансий: {e}")
return []
def _ai_filter_vacancies(self, vacancies: List[Vacancy]) -> List[Vacancy]:
"""AI фильтрация вакансий"""
logger.info("🤖 ЭТАП 2: AI анализ вакансий")
ai_suitable = []
total_count = len(vacancies)
for i, vacancy in enumerate(vacancies, 1):
truncated_name = UIFormatter.truncate_text(vacancy.name)
logger.info(f"Анализ {i}/{total_count}: {truncated_name}...")
try:
if self._get_ai_service().should_apply(vacancy):
ai_suitable.append(vacancy)
logger.info("✅ Добавлено в список для отклика")
else:
logger.info("Не рекомендуется")
if i < total_count:
time.sleep(AppConstants.AI_REQUEST_PAUSE)
except Exception as e:
logger.error(f"Ошибка AI анализа: {e}")
ai_suitable.append(vacancy)
self._log_ai_results(total_count, ai_suitable)
return ai_suitable
def _initialize_browser_and_auth(self) -> bool:
"""Инициализация браузера и авторизация"""
logger.info("🌐 ЭТАП 3: Инициализация браузера и авторизация")
try:
if not self.browser_service.initialize():
logger.error("Не удалось инициализировать браузер")
return False
if not self.browser_service.authenticate_interactive():
logger.error("Не удалось авторизоваться")
return False
logger.info("✅ Браузер готов к работе")
return True
except Exception as e:
logger.error(f"Ошибка инициализации: {e}")
return False
def _apply_to_vacancies(self, vacancies: List[Vacancy]) -> List[ApplicationResult]:
max_successful_apps = settings.application.max_applications
logger.info(
f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)"
)
logger.info("💡 Между заявками добавляются паузы")
logger.info("💡 Лимит считается только по успешным заявкам")
application_results = []
successful_count = 0
processed_count = 0
for vacancy in vacancies:
if successful_count >= max_successful_apps:
logger.info(
f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}"
)
break
processed_count += 1
truncated_name = UIFormatter.truncate_text(vacancy.name, medium=True)
logger.info(
f"Обработка {processed_count}: {truncated_name} "
f"(успешных: {successful_count}/{max_successful_apps})"
)
try:
result = self.browser_service.apply_to_vacancy(vacancy)
application_results.append(result)
self._log_application_result(result)
if result.success:
successful_count += 1
logger.info(
f" 🎉 Успешных заявок: "
f"{successful_count}/{max_successful_apps}"
)
if (
processed_count < len(vacancies)
and successful_count < max_successful_apps
):
self.browser_service.add_random_pause()
except Exception as e:
logger.error(f"Неожиданная ошибка при подаче заявки: {e}")
error_result = ApplicationResult(
vacancy_id="",
vacancy_name=vacancy.name,
success=False,
error_message=str(e),
)
application_results.append(error_result)
logger.info(
f"🏁 Обработка завершена. Обработано вакансий: {processed_count}, "
f"успешных заявок: {successful_count}"
)
return application_results
def _log_search_results(
self, all_vacancies: List[Vacancy], suitable: List[Vacancy]
):
"""Логирование результатов поиска"""
logger.info("📊 Результат базовой фильтрации:")
logger.info(f" 🔍 Всего: {len(all_vacancies)}")
logger.info(f" ✅ Подходящих: {len(suitable)}")
if len(all_vacancies) > 0:
percentage = UIFormatter.format_percentage(
len(suitable), len(all_vacancies)
)
logger.info(f" 📈 % соответствия: {percentage}")
def _log_ai_results(self, total_analyzed: int, ai_suitable: List[Vacancy]):
"""Логирование результатов AI анализа"""
logger.info("🎯 AI фильтрация завершена:")
logger.info(f" 🤖 Проанализировано: {total_analyzed}")
logger.info(f" ✅ Рекомендовано: {len(ai_suitable)}")
if total_analyzed > 0:
percentage = UIFormatter.format_percentage(len(ai_suitable), total_analyzed)
logger.info(f" 📈 % одобрения: {percentage}")
def _log_application_result(self, result: ApplicationResult):
"""Логирование результата подачи заявки"""
if result.success:
logger.info(" ✅ Заявка отправлена успешно")
elif result.already_applied:
logger.info(" ⚠️ Уже откликались ранее")
elif result.skipped:
logger.warning(f" ⏭️ Пропущено: {result.error_message}")
else:
logger.warning(f" ❌ Ошибка: {result.error_message}")
def _create_stats(self, application_results: List[ApplicationResult]) -> Dict:
"""Создание итоговой статистики"""
total_applications = len(application_results)
successful = sum(1 for r in application_results if r.success)
already_applied = sum(1 for r in application_results if r.already_applied)
skipped = sum(1 for r in application_results if r.skipped)
failed = total_applications - successful - already_applied - skipped
return {
"total_applications": total_applications,
"successful": successful,
"failed": failed,
"already_applied": already_applied,
"skipped": skipped,
}
def _cleanup(self):
"""Очистка ресурсов"""
logger.info("🔒 Закрытие браузера...")
self.browser_service.close()
class JobApplicationManager:
"""Главный менеджер для управления процессом поиска и откликов на вакансии"""
def __init__(self):
LoggingConfigurator.setup_logging(
log_file="logs/hh_bot.log", console_output=False
)
self.orchestrator = AutomationOrchestrator()
self.application_results: List[ApplicationResult] = []
def run_automation(
self, keywords: Optional[str] = None, use_ai: bool = True
) -> Dict:
"""Запуск полного цикла автоматизации"""
print("🚀 Запуск автоматизации HH.ru")
print(UIFormatter.create_separator())
stats = self.orchestrator.execute_automation_pipeline(keywords, use_ai)
if "error" not in stats:
self.application_results = []
return stats
def get_application_results(self) -> List[ApplicationResult]:
"""Получение результатов подачи заявок"""
return self.application_results.copy()
def print_detailed_report(self, stats: Dict) -> None:
"""Детальный отчет о работе"""
UIFormatter.print_section_header("📊 ДЕТАЛЬНЫЙ ОТЧЕТ", long=True)
if "error" in stats:
print(f"❌ Ошибка выполнения: {stats['error']}")
return
print(f"📝 Всего попыток подачи заявок: {stats['total_applications']}")
print(f"✅ Успешно отправлено: {stats['successful']}")
print(f"⚠️ Уже откликались ранее: {stats['already_applied']}")
print(f"⏭️ Пропущено (тестовые/ошибки): {stats['skipped']}")
print(f"❌ Неудачных попыток: {stats['failed']}")
if stats["total_applications"] > 0:
success_rate = UIFormatter.format_percentage(
stats["successful"], stats["total_applications"]
)
print(f"📈 Успешность: {success_rate}")
print(UIFormatter.create_separator(long=True))
if stats["successful"] > 0:
print(f"🎉 Отлично! Отправлено {stats['successful']} новых заявок!")
print("💡 Рекомендуется запускать автоматизацию 2-3 раза в день")
elif stats["already_applied"] > 0:
print("💡 На большинство подходящих вакансий уже подавали заявки")
else:
print("😕 Новые заявки не были отправлены")
print("💡 Попробуйте изменить ключевые слова поиска или настройки")