248 lines
7.8 KiB
Python
248 lines
7.8 KiB
Python
"""
|
||
📋 Модели данных для работы с вакансиями HH.ru
|
||
"""
|
||
|
||
from dataclasses import dataclass, field
|
||
from typing import List, Dict, Optional, Any
|
||
import re
|
||
|
||
|
||
@dataclass
|
||
class Employer:
|
||
"""Информация о работодателе"""
|
||
|
||
id: str
|
||
name: str
|
||
url: Optional[str] = None
|
||
alternate_url: Optional[str] = None
|
||
logo_urls: Optional[Dict[str, str]] = None
|
||
vacancies_url: Optional[str] = None
|
||
trusted: bool = False
|
||
|
||
|
||
@dataclass
|
||
class Experience:
|
||
"""Информация об опыте работы"""
|
||
|
||
id: str
|
||
name: str
|
||
|
||
|
||
@dataclass
|
||
class Snippet:
|
||
"""Краткая информация о вакансии"""
|
||
|
||
requirement: Optional[str] = None
|
||
responsibility: Optional[str] = None
|
||
|
||
|
||
@dataclass
|
||
class Salary:
|
||
"""Информация о зарплате"""
|
||
|
||
from_value: Optional[int] = None
|
||
to_value: Optional[int] = None
|
||
currency: str = "RUR"
|
||
gross: bool = False
|
||
|
||
|
||
@dataclass
|
||
class Vacancy:
|
||
"""Модель вакансии HH.ru"""
|
||
|
||
id: str
|
||
name: str
|
||
alternate_url: str
|
||
employer: Employer
|
||
experience: Experience
|
||
snippet: Snippet
|
||
premium: bool = False
|
||
has_test: bool = False
|
||
response_letter_required: bool = False
|
||
archived: bool = False
|
||
apply_alternate_url: Optional[str] = None
|
||
|
||
ai_match_score: Optional[float] = None
|
||
ai_match_reasons: List[str] = field(default_factory=list)
|
||
|
||
salary: Optional[Salary] = None
|
||
|
||
@classmethod
|
||
def from_api_response(cls, data: Dict[str, Any]) -> "Vacancy":
|
||
"""Создание экземпляра из ответа API HH.ru"""
|
||
try:
|
||
|
||
employer_data = data.get("employer", {})
|
||
employer = Employer(
|
||
id=employer_data.get("id", ""),
|
||
name=employer_data.get("name", "Неизвестная компания"),
|
||
url=employer_data.get("url"),
|
||
alternate_url=employer_data.get("alternate_url"),
|
||
logo_urls=employer_data.get("logo_urls"),
|
||
vacancies_url=employer_data.get("vacancies_url"),
|
||
trusted=employer_data.get("trusted", False),
|
||
)
|
||
|
||
experience_data = data.get("experience", {})
|
||
experience = Experience(
|
||
id=experience_data.get("id", "noExperience"),
|
||
name=experience_data.get("name", "Без опыта"),
|
||
)
|
||
|
||
snippet_data = data.get("snippet", {})
|
||
snippet = Snippet(
|
||
requirement=snippet_data.get("requirement"),
|
||
responsibility=snippet_data.get("responsibility"),
|
||
)
|
||
|
||
salary = None
|
||
salary_data = data.get("salary")
|
||
if salary_data:
|
||
salary = Salary(
|
||
from_value=salary_data.get("from"),
|
||
to_value=salary_data.get("to"),
|
||
currency=salary_data.get("currency", "RUR"),
|
||
gross=salary_data.get("gross", False),
|
||
)
|
||
|
||
return cls(
|
||
id=data.get("id", ""),
|
||
name=data.get("name", "Без названия"),
|
||
alternate_url=data.get("alternate_url", ""),
|
||
employer=employer,
|
||
experience=experience,
|
||
snippet=snippet,
|
||
premium=data.get("premium", False),
|
||
has_test=data.get("has_test", False),
|
||
response_letter_required=data.get("response_letter_required", False),
|
||
archived=data.get("archived", False),
|
||
apply_alternate_url=data.get("apply_alternate_url"),
|
||
salary=salary,
|
||
)
|
||
except Exception as e:
|
||
print(f"❌ Ошибка парсинга вакансии: {e}")
|
||
|
||
return cls(
|
||
id=data.get("id", "unknown"),
|
||
name=data.get("name", "Ошибка загрузки"),
|
||
alternate_url=data.get("alternate_url", ""),
|
||
employer=Employer(id="", name="Неизвестно"),
|
||
experience=Experience(id="noExperience", name="Без опыта"),
|
||
snippet=Snippet(),
|
||
)
|
||
|
||
def has_python(self) -> bool:
|
||
"""Проверка упоминания Python в вакансии"""
|
||
text_to_check = (
|
||
f"{self.name} {self.snippet.requirement or ''} " f"{self.snippet.responsibility or ''}"
|
||
)
|
||
python_patterns = [
|
||
r"\bpython\b",
|
||
r"\bпайтон\b",
|
||
r"\bджанго\b",
|
||
r"\bflask\b",
|
||
r"\bfastapi\b",
|
||
r"\bpandas\b",
|
||
r"\bnumpy\b",
|
||
]
|
||
|
||
for pattern in python_patterns:
|
||
if re.search(pattern, text_to_check, re.IGNORECASE):
|
||
return True
|
||
return False
|
||
|
||
def is_junior_level(self) -> bool:
|
||
"""Проверка на junior уровень"""
|
||
junior_keywords = [
|
||
"junior",
|
||
"джуниор",
|
||
"стажер",
|
||
"стажёр",
|
||
"начинающий",
|
||
"intern",
|
||
"trainee",
|
||
"entry",
|
||
"младший",
|
||
]
|
||
|
||
text_to_check = f"{self.name} {self.snippet.requirement or ''}"
|
||
|
||
for keyword in junior_keywords:
|
||
if keyword.lower() in text_to_check.lower():
|
||
return True
|
||
return False
|
||
|
||
def get_salary_info(self) -> str:
|
||
"""Получение информации о зарплате в читаемом виде"""
|
||
if not self.salary:
|
||
return "Зарплата не указана"
|
||
|
||
from_val = self.salary.from_value
|
||
to_val = self.salary.to_value
|
||
currency = self.salary.currency
|
||
gross_suffix = " (до вычета налогов)" if self.salary.gross else " (на руки)"
|
||
|
||
if from_val and to_val:
|
||
return f"{from_val:,} - {to_val:,} {currency}{gross_suffix}"
|
||
elif from_val:
|
||
return f"от {from_val:,} {currency}{gross_suffix}"
|
||
elif to_val:
|
||
return f"до {to_val:,} {currency}{gross_suffix}"
|
||
else:
|
||
return "Зарплата не указана"
|
||
|
||
def get_full_text(self) -> str:
|
||
"""Получение полного текста вакансии для анализа"""
|
||
text_parts = [
|
||
self.name,
|
||
self.employer.name,
|
||
self.snippet.requirement or "",
|
||
self.snippet.responsibility or "",
|
||
self.experience.name,
|
||
]
|
||
return " ".join(filter(None, text_parts))
|
||
|
||
|
||
@dataclass
|
||
class ApplicationResult:
|
||
"""Результат подачи заявки на вакансию"""
|
||
|
||
vacancy_id: str
|
||
vacancy_name: str
|
||
success: bool
|
||
already_applied: bool = False
|
||
error_message: Optional[str] = None
|
||
timestamp: Optional[str] = None
|
||
|
||
def __post_init__(self):
|
||
"""Устанавливаем timestamp если не указан"""
|
||
if self.timestamp is None:
|
||
from datetime import datetime
|
||
|
||
self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
|
||
@dataclass
|
||
class SearchStats:
|
||
"""Статистика поиска вакансий"""
|
||
|
||
total_found: int = 0
|
||
pages_processed: int = 0
|
||
filtered_count: int = 0
|
||
python_vacancies: int = 0
|
||
junior_vacancies: int = 0
|
||
with_salary: int = 0
|
||
without_test: int = 0
|
||
|
||
def __str__(self) -> str:
|
||
return f"""
|
||
📊 Статистика поиска:
|
||
📋 Всего найдено: {self.total_found}
|
||
📄 Страниц обработано: {self.pages_processed}
|
||
✅ Прошло фильтрацию: {self.filtered_count}
|
||
🐍 Python вакансий: {self.python_vacancies}
|
||
👶 Junior уровня: {self.junior_vacancies}
|
||
💰 С указанной ЗП: {self.with_salary}
|
||
📝 Без тестов: {self.without_test}
|
||
"""
|