Локальное self-hosted приложение для упорядочивания личного поиска работы на hh.ru.
Тянет вакансии и личные данные (отклики, резюме, индекс вежливости работодателей) через парсинг страниц hh.ru под сессионной кукой залогиненного браузера, агрегирует в SQLite и отдаёт через FastAPI + Jinja2 + HTMX. Считает match-score, предсказывает вероятность приглашения, ведёт воронку откликов.
🔗 progl.github.io/hh_job_tracker — статический снапшот UI на демо-данных (data/hh_demo.db). Интерактив (фильтры, кнопки, формы) отключён — это снапшот, не работающее приложение; для полного функционала подними локально через make demo-seed && make demo-run.
Все скриншоты сняты на demo-режиме (
make demo-seed && make demo-run) — данные вымышленные, реальная БД в публикацию не попадает.
| Главная — таблица + воронка + фильтры | Воронка откликов |
|---|---|
| Профиль (импорт из резюме) | Деталка вакансии: разбивка match-score |
|---|---|
| Архивная вакансия | Лог HTTP-запросов к HH |
|---|---|
Проблема. Активный поиск работы на hh.ru — это сотни вкладок, неотсортированный inbox откликов, забытые приглашения, выбор между «100 хороших» и «1000 шумных». UI hh.ru заточен под массового соискателя, а не под того, кто целенаправленно ищет роль с конкретным стеком, ЗП и форматом.
Идея. Один локальный инструмент, который:
- видит твою воронку — все отклики с реальными состояниями (RESPONSE/INVITATION/INTERVIEW/DISCARD), а не «непрочитанные»
- ранжирует вакансии под тебя — match-score 0–100 на базе твоего же импортированного резюме, не общая «релевантность» от HH
- предсказывает шанс приглашения — predict % через эвристику, дальше — sklearn LogisticRegression на твоей реальной истории
- знает работодателя — индекс вежливости HH Pro (% прочитанных откликов, среднее время ответа) встроен в скоринг
- держит дисциплину — фоновый sync каждые 4–6 часов, дедуп, авто-архив, история смены статусов
Почему «Tracker», а не «Scraper». Скрейпинг тут — побочный механизм доступа к собственным данным после закрытия OAuth-API (15.12.2025). Главное — трекинг: воронка, статусы, скоринг, predict. Поэтому одна сессия = один пользователь, никаких массовых прогонов, никакого распространения данных.
Почему self-hosted. Сессионная кука HH Pro — это полный доступ к личному кабинету. Её нельзя отдавать SaaS. БД и ML-модель остаются в data/ локально, ничего не уходит наружу.
Почему не пишет на HH. Никаких авто-откликов, авто-приглашений, авто-сообщений. Только чтение + локальная обработка. Это и этика, и техническая гарантия что аккаунт не словит бан за «бота».
С 15.12.2025 HH закрыл OAuth API для соискателей: форма регистрации новых приложений не работает, а публичный api.hh.ru/vacancies отдаёт 403 без авторизации. Доступ к собственным откликам и резюме через API стал невозможен.
Работает: api.hh.ru/areas (справочники регионов, без auth). Всё остальное — через hh.ru под сессионной кукой залогиненного браузера. Подробнее: docs/scraping.md и docs/antibot.md.
- Сбор вакансий: одноразовый по запросу + сохранённые поиски с автосинком каждые 4 часа
- Личные данные: 100% откликов с состояниями (RESPONSE/INVITATION/INTERVIEW/DISCARD), история смены статусов, индекс вежливости работодателей (HH Pro)
- Profile-driven match-score (0–100): стек × ЗП × формат × опыт × вежливость × конкуренция
- Predict %: вероятность приглашения. Эвристика по умолчанию; sklearn LogisticRegression обучается на реальной истории откликов, когда наберётся ≥10 positive исходов
- Воронка: 6 счётчиков, топ работодателей, среднее время до ответа HR, гистограмма по неделям
- Фильтры: статус (include/exclude), отклик HH (include/exclude), уровень, стек, ЗП, формат, текст, сортировка по любой колонке. Состояние пишется в URL — sharable
- Быстрая разметка: dropdown статусов + одно-кликовая кнопка 🗑 «Скип»
- Архивы: вакансии помеченные «Скип», «снятые с HH» и «в архиве на HH» (работодатель закрыл приём откликов) автоматически прячутся из выдачи. У каждого типа свой бейдж в строке (
⌫ снята/📦 архив) и отдельная кнопка-фильтр (hide → only → all) - Авто-дедуп: уже подтянутые вакансии исключаются из повторного backfill, удалённые с HH помечаются
disappeared_at, архивные —archived_at. ДополнительноPOST /api/dedupпомечает дубликаты по нормализованной паре (название + компания) как «Скип» - Сравнение: до 6 вакансий поколоночно с подсветкой различий
- ЦБ курсы: ЗП в любой валюте → рубли по актуальному курсу (cbr-xml-daily.ru, кеш 24ч)
- Тёмная тема, экспорт CSV/JSON, страница логов запросов с фильтрами и счётчиками
- Python 3.12 (строго
>=3.12,<3.13— на 3.14 jinja2 кеш ломается) - FastAPI, Uvicorn, httpx (HTTP/2, async)
- SQLite (aiosqlite), Jinja2 + HTMX + Tailwind через CDN
- apscheduler, scikit-learn, joblib
- pytest + pytest-asyncio + pytest-cov (237 тестов, 66% покрытие, см. docs/testing.md)
- GitHub Actions CI — прогон на каждый push/PR (
.github/workflows/test.yml)
curl -LsSf https://astral.sh/uv/install.sh | sh # или: brew install uv
cp .env.example .env # заполнить — см. ниже
uv sync # Python 3.12 + зависимости в .venv
make run # http://127.0.0.1:8000/- Открой hh.ru в Chrome/Safari, залогинься (под Pro-аккаунтом, если он есть)
- DevTools (
Cmd+Opt+I) → Network - Сделай любое действие на странице (клик, скролл) — появятся запросы
- Правый клик на любой запрос к
hh.ru→ Copy → Copy as cURL - Из cURL вытащить:
-b 'hhuid=...; hhtoken=...; ...'→ HH_COOKIE-H 'user-agent: ...'→ HH_USER_AGENT-H 'sec-ch-ua: ...'→ HH_SEC_CH_UA- URL вида
https://<region>.hh.ru/...→ HH_BASE_URL (напримерhttps://moscow.hh.ru)
Открыть проект → Add Interpreter → Select existing → .venv/bin/python.
Run Configuration: Module name uvicorn, Parameters app.web.app:app --reload --host 127.0.0.1 --port 8000, Working directory — корень проекта. Жми ▶ или 🐞 Debug.
Альтернатива: плагин Makefile language → запуск таргета run из правой панели.
После запуска UI:
- Открой
/→ справа внизу панель «Статус» должна показатьсостояние: активен, ~30+ кук - В панели «Статус» → раздел «Расписание» → жми ▶ рядом с
Обновлять отклики (инкрем.). Через ~30 сек подтянутся отклики, профиль импортируется из резюме, увидишь воронку наверху - Сохрани поиск: «Сохранённые поиски» →
+ Сохранить новый(имя, текст, регион, ✓ только удалёнка) - Жми ▶ на сохранённом поиске → подтянет вакансии
- Жми «Подтянуть вакансии» (или ▶ в Расписании) → дотянет полные карточки из откликов
После — таблица будет полная, match-score и predict % считаются на основе импортированного резюме.
app/ — FastAPI app: clients, collector, db, parsers, scoring, web
data/ — SQLite БД + ML model + dataset (gitignored)
scripts/ — export_dataset.py, seed_demo.py
tests/ — pytest: unit / integration / e2e (237 тестов, 66% coverage)
.github/workflows/ — GitHub Actions CI (pytest на каждый push)
docs/ — внутренняя документация
docs/images/ — скрины UI на demo-данных
Подробное описание архитектуры, точек входа и фоновых задач — docs/architecture.md.
Отдельная БД data/hh_demo.db с вымышленными данными — UI без своих откликов и компаний. Реальная data/hh.db не трогается.
make demo-seed # создать/пересоздать demo (1 профиль, 8 компаний, 20 вакансий, 15 откликов)
make demo-run # запустить uvicorn на demo-БД
make demo-clean # удалить demo-БДВ demo включены оба пограничных случая: 2 архивных вакансии и 2 снятых с HH — для проверки бейджей и работы фильтров.
Защита от перезаписи. scripts/seed_demo.py жёстко прибит к пути data/hh_demo.db и в начале _assert_not_real_db() проверяет что путь не совпадает с settings.DB_PATH — даже при подмене env-переменной не запишет в data/hh.db.
make test # 237 тестов с -v, ~10 сек
make coverage # + HTML отчёт в htmlcov/index.htmlСлои: 121 unit (парсеры, скоринг, детектор архивности, ml, tasks, scheduler, rate-limit, events) + 102 integration (репозитории, cookies, cbr, дедуп, favorites) + 14 e2e (FastAPI через httpx.ASGITransport с заглушенным lifespan).
Покрытие — 66% общее, ключевые модули 88–100%. CI: GitHub Actions прогоняет всё на каждый push/PR. Подробнее: docs/testing.md.
| Раздел | Файл |
|---|---|
| Как происходит скрейпинг (state, эндпоинты, пайплайн) | docs/scraping.md |
| Антибот (rate-limit, headers, referer, cookie jar, ветвление 403) | docs/antibot.md |
| Архитектура (структура, точки входа, apscheduler) | docs/architecture.md |
| Тесты и покрытие | docs/testing.md |
| Когда что-то ломается | docs/troubleshooting.md |
.envс кукой/UA в.gitignore— не коммититсяdata/целиком в.gitignore— БД, dataset, ML model остаются локально- Куки автоматически обновляются (
Set-Cookieот HH) и сохраняются вcookie_storeмежду перезапусками - При смене
hhtokenв.env— локальный кеш jar автоматически сбрасывается - Все запросы к HH логируются в
request_logs—/logsс фильтрами
MIT — см. LICENSE.