Полуавтоматический отклик на hh.ru: поиск, ИИ-генерация, веб-интерфейс
This commit is contained in:
commit
0ca55f3818
11 files changed
+1740
No files matched your search
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Работа с браузером: открытие вакансии, клик «Откликнуться», вставка текста."""
|
||||
import os
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
# Селекторы hh.ru (с запасными вариантами)
|
||||
RESPONSE_BUTTONS = [
|
||||
'[data-qa="vacancy-response-link-top"]',
|
||||
'[data-qa="vacancy-response-link"]',
|
||||
'button:has-text("Откликнуться")',
|
||||
]
|
||||
MESSAGE_FIELDS = [
|
||||
'textarea[data-qa="vacancy-response-letter"]',
|
||||
'textarea[data-qa="vacancy-response-popup-form"] textarea',
|
||||
'textarea',
|
||||
]
|
||||
|
||||
# Живые браузерные контексты по аккаунтам: {account_name: context}
|
||||
_browsers = {}
|
||||
|
||||
|
||||
def launch_context(account_dir):
|
||||
"""Запускает браузер с профилем аккаунта (видимое окно)."""
|
||||
p = sync_playwright().start()
|
||||
context = p.chromium.launch_persistent_context(
|
||||
user_data_dir=os.path.join(account_dir, "profile"),
|
||||
headless=False,
|
||||
viewport={"width": 1400, "height": 900},
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
def get_context(account_dir, account_name):
|
||||
"""Возвращает живой контекст аккаунта, создавая при необходимости."""
|
||||
if account_name not in _browsers:
|
||||
_browsers[account_name] = launch_context(account_dir)
|
||||
return _browsers[account_name]
|
||||
|
||||
|
||||
def close_context(account_name):
|
||||
if account_name in _browsers:
|
||||
try:
|
||||
_browsers[account_name].close()
|
||||
except Exception:
|
||||
pass
|
||||
_browsers.pop(account_name, None)
|
||||
|
||||
|
||||
def open_response_page(context, vacancy, message, resume_id=None):
|
||||
"""Открывает вакансию, жмёт «Откликнуться», выбирает резюме и вставляет текст.
|
||||
Возвращает (ok, note): ok=True если текст вставлен автоматически."""
|
||||
page = context.new_page()
|
||||
page.goto(vacancy["url"], timeout=45000)
|
||||
page.wait_for_timeout(2500)
|
||||
|
||||
# Кнопка «Откликнуться»
|
||||
clicked = False
|
||||
for sel in RESPONSE_BUTTONS:
|
||||
try:
|
||||
btn = page.locator(sel).first
|
||||
if btn.is_visible(timeout=3000):
|
||||
btn.click()
|
||||
clicked = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not clicked:
|
||||
return False, "Не нашёл кнопку «Откликнуться» — сделайте это вручную"
|
||||
|
||||
# Ждём модалку
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
# Выбор резюме (радио-кнопка name="resumeId" value="{hash}")
|
||||
if resume_id:
|
||||
try:
|
||||
radio = page.locator(f'input[name="resumeId"][value="{resume_id}"]').first
|
||||
if radio.is_visible(timeout=3000):
|
||||
radio.check(force=True)
|
||||
print(f" ✓ Резюме {resume_id} выбрано")
|
||||
except Exception:
|
||||
print(" ⚠ Не нашёл радио-кнопку резюме — выберите вручную")
|
||||
|
||||
# Вставляем текст
|
||||
for sel in MESSAGE_FIELDS:
|
||||
try:
|
||||
field = page.locator(sel).first
|
||||
if field.is_visible(timeout=3000):
|
||||
field.fill(message)
|
||||
return True, "Текст вставлен. Проверьте и нажмите «Отправить»"
|
||||
except Exception:
|
||||
continue
|
||||
return False, "Не нашёл поле сообщения — вставьте текст вручную"
|
||||
Reference in new issue
Block a user