""" πŸ” БСрвис для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с API HH.ru """ 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]: """Поиск вакансий Ρ‡Π΅Ρ€Π΅Π· API""" 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, "experience": config.experience, "per_page": str(config.per_page), "page": str(page), "order_by": config.order_by, "employment": "full,part", "schedule": "fullDay,remote,flexible", "only_with_salary": "false", } return params class QueryBuilder: """ΠžΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ Π·Π° построСниС поисковых запросов""" @staticmethod def build_search_query(keywords: str) -> str: """ΠŸΠΎΡΡ‚Ρ€ΠΎΠ΅Π½ΠΈΠ΅ ΡƒΠΌΠ½ΠΎΠ³ΠΎ поискового запроса""" base_queries = [ keywords, f"{keywords} junior", f"{keywords} стаТСр", f"{keywords} Π½Π°Ρ‡ΠΈΠ½Π°ΡŽΡ‰ΠΈΠΉ", f"{keywords} Π±Π΅Π· ΠΎΠΏΡ‹Ρ‚Π°", ] return " OR ".join(f"({query})" for query in base_queries) @staticmethod def suggest_keywords(base_keyword: str = "python") -> List[str]: """ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΡ ΠΊΠ»ΡŽΡ‡Π΅Π²Ρ‹Ρ… слов для поиска""" return [ f"{base_keyword} junior", f"{base_keyword} стаТСр", f"{base_keyword} django", f"{base_keyword} flask", f"{base_keyword} fastapi", f"{base_keyword} web", f"{base_keyword} backend", f"{base_keyword} Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ", f"{base_keyword} developer", f"{base_keyword} программист", ] class VacancyFilter: """ΠžΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ Π·Π° Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡŽ вакансий""" EXCLUDE_KEYWORDS = [ "senior", "lead", "ΡΡ‚Π°Ρ€ΡˆΠΈΠΉ", "Π²Π΅Π΄ΡƒΡ‰ΠΈΠΉ", "Π³Π»Π°Π²Π½Ρ‹ΠΉ", "team lead", "tech lead", "Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΠΎΡ€", "head", "Ρ€ΡƒΠΊΠΎΠ²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒ", "manager", "director", ] @staticmethod def filter_suitable(vacancies: List[Vacancy]) -> List[Vacancy]: """Π€ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ подходящих вакансий""" suitable = [] for vacancy in vacancies: if VacancyFilter._is_suitable_basic(vacancy): suitable.append(vacancy) print(f"βœ… ПослС Π±Π°Π·ΠΎΠ²ΠΎΠΉ Ρ„ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΠΈ: {len(suitable)} подходящих вакансий") return suitable @staticmethod def _is_suitable_basic(vacancy: Vacancy) -> bool: """Базовая ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° подходящСсти вакансии""" if not vacancy.has_python(): print(f"❌ ΠŸΡ€ΠΎΠΏΡƒΡΠΊΠ°Π΅ΠΌ '{vacancy.name}' - Π½Π΅Ρ‚ Python") return False text = vacancy.get_full_text().lower() for exclude in VacancyFilter.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: """Π“Π»Π°Π²Π½Ρ‹ΠΉ сСрвис для Ρ€Π°Π±ΠΎΡ‚Ρ‹ с API HH.ru""" 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 ) -> List[Vacancy]: """Π€ΠΈΠ»ΡŒΡ‚Ρ€Π°Ρ†ΠΈΡ подходящих вакансий""" if not use_basic_filter: return vacancies suitable = self.filter.filter_suitable(vacancies) 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() def suggest_keywords(self, base_keyword: str = "python") -> List[str]: """ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΡ ΠΊΠ»ΡŽΡ‡Π΅Π²Ρ‹Ρ… слов для поиска""" return QueryBuilder.suggest_keywords(base_keyword)