hh-bot/hh_bot/services/hh_api_service.py

196 lines
6.4 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 requests
import time
from typing import List, Dict, Any, Optional
import traceback
from ..config.settings import settings, AppConstants
from ..models.vacancy import Vacancy, SearchStats
class VacancySearcher:
def __init__(self):
self.base_url = AppConstants.HH_BASE_URL
self.headers = {"User-Agent": "HH-Search-Bot/2.0 (job-search-automation)"}
def search(self, keywords: Optional[str] = None) -> List[Vacancy]:
if keywords:
settings.update_search_keywords(keywords)
print(f"🔍 Поиск вакансий: {settings.hh_search.keywords}")
all_vacancies = []
try:
for page in range(settings.hh_search.max_pages):
print(f"📄 Обработка страницы {page + 1}...")
page_vacancies = self._fetch_page(page)
if not page_vacancies:
print(f"⚠️ Страница {page + 1} пуста, прекращаем поиск")
break
all_vacancies.extend(page_vacancies)
print(
f"📋 Найдено {len(page_vacancies)} вакансий на странице {page + 1}"
)
time.sleep(AppConstants.API_PAUSE_SECONDS)
print(f"\n📊 Всего найдено: {len(all_vacancies)} вакансий")
return all_vacancies
except Exception as e:
print(f"❌ Ошибка поиска вакансий: {e}")
print(f"🔍 Traceback: {traceback.format_exc()}")
return []
def _fetch_page(self, page: int) -> List[Vacancy]:
params = self._build_search_params(page)
try:
response = requests.get(
f"{self.base_url}/vacancies",
params=params,
headers=self.headers,
timeout=AppConstants.DEFAULT_TIMEOUT,
)
response.raise_for_status()
data = response.json()
items = data.get("items", [])
vacancies = []
for item in items:
try:
vacancy = Vacancy.from_api_response(item)
vacancies.append(vacancy)
except Exception as e:
print(f"⚠️ Ошибка парсинга вакансии: {e}")
continue
return vacancies
except requests.RequestException as e:
print(f"❌ Ошибка запроса к API HH.ru: {e}")
return []
except Exception as e:
print(f"❌ Неожиданная ошибка при получении страницы: {e}")
return []
def _build_search_params(self, page: int) -> Dict[str, str]:
config = settings.hh_search
search_query = QueryBuilder.build_search_query(config.keywords)
params = {
"text": search_query,
"area": config.area,
"per_page": str(min(config.per_page, 20)),
"page": str(page),
"order_by": "publication_time",
}
return params
class QueryBuilder:
@staticmethod
def build_search_query(keywords: str) -> str:
return keywords
class VacancyFilter:
@staticmethod
def filter_suitable(
vacancies: List[Vacancy], search_keywords: str = ""
) -> List[Vacancy]:
suitable = []
for vacancy in vacancies:
if VacancyFilter._is_suitable_basic(vacancy, search_keywords):
suitable.append(vacancy)
print(f"✅ После базовой фильтрации: {len(suitable)} подходящих вакансий")
return suitable
@staticmethod
def _is_suitable_basic(vacancy: Vacancy, search_keywords: str = "") -> bool:
if search_keywords and not vacancy.matches_keywords(search_keywords):
print(f"❌ Пропускаем '{vacancy.name}' - не соответствует ключевым словам")
return False
if not search_keywords and not vacancy.has_python():
print(f"❌ Пропускаем '{vacancy.name}' - нет Python")
return False
text = vacancy.get_full_text().lower()
for exclude in settings.get_exclude_keywords():
if exclude in text:
print(f"❌ Пропускаем '{vacancy.name}' - содержит '{exclude}'")
return False
if vacancy.archived:
print(f"❌ Пропускаем '{vacancy.name}' - архивная")
return False
print(f"✅ Подходящая вакансия: '{vacancy.name}'")
return True
class VacancyDetailsFetcher:
def __init__(self):
self.base_url = AppConstants.HH_BASE_URL
self.headers = {"User-Agent": "HH-Search-Bot/2.0 (job-search-automation)"}
def get_details(self, vacancy_id: str) -> Optional[Dict[str, Any]]:
try:
response = requests.get(
f"{self.base_url}/vacancies/{vacancy_id}",
headers=self.headers,
timeout=AppConstants.DEFAULT_TIMEOUT,
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"❌ Ошибка получения деталей вакансии {vacancy_id}: {e}")
return None
class HHApiService:
def __init__(self):
self.searcher = VacancySearcher()
self.filter = VacancyFilter()
self.details_fetcher = VacancyDetailsFetcher()
self.stats = SearchStats()
def search_vacancies(self, keywords: Optional[str] = None) -> List[Vacancy]:
vacancies = self.searcher.search(keywords)
self.stats.total_found = len(vacancies)
return vacancies
def filter_suitable_vacancies(
self,
vacancies: List[Vacancy],
use_basic_filter: bool = True,
search_keywords: str = "",
) -> List[Vacancy]:
if not use_basic_filter:
return vacancies
suitable = self.filter.filter_suitable(vacancies, search_keywords)
self.stats.filtered_count = len(suitable)
return suitable
def get_vacancy_details(self, vacancy_id: str) -> Optional[Dict[str, Any]]:
return self.details_fetcher.get_details(vacancy_id)
def get_search_stats(self) -> SearchStats:
return self.stats
def reset_stats(self) -> None:
self.stats = SearchStats()