hh-bot/hh_bot/core/job_application_manager.py

307 lines
13 KiB
Python
Raw Permalink Normal View History

2025-06-27 09:57:34 +02:00
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
2025-06-27 09:57:34 +02:00
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():
2025-06-28 18:51:41 +02:00
vacancies = self._ai_filter_vacancies(vacancies)
2025-06-27 09:57:34 +02:00
if not vacancies:
return {"error": "После AI фильтрации подходящих вакансий нет"}
2025-06-27 09:57:34 +02:00
init_ok = self._initialize_browser_and_auth()
if not init_ok:
2025-06-27 09:57:34 +02:00
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]:
2025-06-27 09:57:34 +02:00
"""Поиск и базовая фильтрация вакансий"""
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 ""
)
2025-06-27 09:57:34 +02:00
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):
2025-06-27 09:57:34 +02:00
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
2025-06-27 09:57:34 +02:00
logger.info(
f"📨 ЭТАП 4: Подача заявок (максимум {max_successful_apps} успешных)"
)
logger.info("💡 Между заявками добавляются паузы")
logger.info("💡 Лимит считается только по успешным заявкам")
2025-06-27 09:57:34 +02:00
application_results = []
successful_count = 0
processed_count = 0
2025-06-27 09:57:34 +02:00
for vacancy in vacancies:
if successful_count >= max_successful_apps:
logger.info(
f"🎯 Достигнут лимит успешных заявок: {max_successful_apps}"
)
break
processed_count += 1
2025-06-27 09:57:34 +02:00
truncated_name = UIFormatter.truncate_text(vacancy.name, medium=True)
logger.info(
f"Обработка {processed_count}: {truncated_name} "
f"(успешных: {successful_count}/{max_successful_apps})"
)
2025-06-27 09:57:34 +02:00
try:
result = self.browser_service.apply_to_vacancy(vacancy)
2025-06-27 09:57:34 +02:00
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
):
2025-06-27 09:57:34 +02:00
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}"
)
2025-06-27 09:57:34 +02:00
return application_results
def _log_search_results(
self, all_vacancies: List[Vacancy], suitable: List[Vacancy]
):
2025-06-27 09:57:34 +02:00
"""Логирование результатов поиска"""
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)
)
2025-06-27 09:57:34 +02:00
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}")
2025-06-27 09:57:34 +02:00
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
2025-06-27 09:57:34 +02:00
return {
"total_applications": total_applications,
"successful": successful,
"failed": failed,
"already_applied": already_applied,
"skipped": skipped,
2025-06-27 09:57:34 +02:00
}
def _cleanup(self):
"""Очистка ресурсов"""
logger.info("🔒 Закрытие браузера...")
self.browser_service.close()
class JobApplicationManager:
"""Главный менеджер для управления процессом поиска и откликов на вакансии"""
def __init__(self):
LoggingConfigurator.setup_logging(
2025-06-28 19:35:35 +02:00
log_file="logs/hh_bot.log", console_output=True
)
2025-06-27 09:57:34 +02:00
self.orchestrator = AutomationOrchestrator()
self.application_results: List[ApplicationResult] = []
def run_automation(
self, keywords: Optional[str] = None, use_ai: bool = True
) -> Dict:
2025-06-27 09:57:34 +02:00
"""Запуск полного цикла автоматизации"""
print("🚀 Запуск автоматизации HH.ru")
print(UIFormatter.create_separator())
stats = self.orchestrator.execute_automation_pipeline(keywords, use_ai)
if "error" not in stats:
self.application_results = []
2025-06-27 09:57:34 +02:00
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']}")
2025-06-27 09:57:34 +02:00
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("💡 Попробуйте изменить ключевые слова поиска или настройки")