From 5e2a1514c5102c2563b3bb0ac130ae18c8cf75c8 Mon Sep 17 00:00:00 2001 From: stackops Date: Mon, 24 Aug 2026 20:34:25 +0300 Subject: [PATCH] hh resume booster: free auto-bump for hh.ru (Playwright, Linux/Win/Mac) --- .gitignore | 6 + README.md | 207 ++++++++++++++++++++++++++++ bump_resumes.py | 115 ++++++++++++++++ config.example.json | 5 + hh-resume-booster.zip | Bin 0 -> 14310 bytes login.py | 49 +++++++ requirements.txt | 1 + scheduler/com.stackops.hhbump.plist | 17 +++ scheduler/hhbump.service | 7 + scheduler/hhbump.timer | 10 ++ scheduler/install-linux.sh | 15 ++ scheduler/install-mac.sh | 12 ++ scheduler/install-windows.ps1 | 16 +++ scheduler/run.bat | 4 + scheduler/run.sh | 4 + 15 files changed, 468 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bump_resumes.py create mode 100644 config.example.json create mode 100644 hh-resume-booster.zip create mode 100644 login.py create mode 100644 requirements.txt create mode 100644 scheduler/com.stackops.hhbump.plist create mode 100644 scheduler/hhbump.service create mode 100644 scheduler/hhbump.timer create mode 100644 scheduler/install-linux.sh create mode 100644 scheduler/install-mac.sh create mode 100644 scheduler/install-windows.ps1 create mode 100644 scheduler/run.bat create mode 100755 scheduler/run.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..475724b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# НИКОГДА не коммитьте и не пересылайте сессию — это полный доступ к аккаунту hh! +session/ +config.json +*.log +.venv/ +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eb8979 --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# hh resume booster + +Бесплатное автоматическое «поднятие» резюме в поиске hh.ru — по расписанию, на Linux, Windows и macOS. + +## Как это работает + +На hh **любое сохранение резюме заново поднимает его в выдаче поиска** — причём +**без 4-часового кулдауна**, который есть у ручной кнопки «Поднять в поиске». +Скрипт вносит невидимую правку в блок **«О себе»** (тумблерит один двойной пробел +в середине текста — в вёрстке страницы он схлопывается, так что глазами он не виден +и содержимое резюме не меняется) и сохраняет. Это бесплатный аналог платного +сервиса «Автоподнятие». + +Резюме на вашем аккаунте определяются **автоматически**. Планировщик ОС гоняет +скрипт каждые 2 часа. + +> ⚠️ **Требование:** у резюме должен быть непустой блок «О себе» (иначе прятать +> правку негде — такое резюме скрипт пропустит и напишет об этом в лог). + +## 🔐 Безопасность — прочитайте + +Файл **`session/storage.json`** — это ваши cookie-сессии hh, то есть **полный доступ +к аккаунту**. Никогда не выкладывайте его в git и не пересылайте архив ПОСЛЕ логина. +В поставке этого файла нет — вы создаёте его локально сам через `login.py`. +`.gitignore` уже исключает `session/`, `config.json` и логи. + +Автоматизация браузером — «серая зона» правил hh (ToS). Риск низкий (вы правите +своё же резюме), но частить не стоит: 2 часа — норм, каждые 5 минут — палево. + +--- + +## 1. Требования + +- **Python 3.9+** +- Интернет и аккаунт соискателя на hh.ru +- ~400 МБ на браузер Playwright (chromium) + +Проверить питон: `python3 --version` (на Windows — `python --version`). + +--- + +## 2. Установка (суть одна, команды под каждую ОС ниже) + +Распакуйте архив, зайдите в папку `hh-resume-booster`, создайте виртуальное +окружение, поставьте зависимости и браузер. + +### 🐧 Linux + +```bash +cd hh-resume-booster +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +playwright install --with-deps chromium # --with-deps ставит системные библиотеки (нужен sudo) +``` +Если `--with-deps` ругается на права — выполните `sudo $(which playwright) install-deps` +один раз, затем `playwright install chromium`. + +### 🪟 Windows (PowerShell) + +```powershell +cd hh-resume-booster +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +playwright install chromium +``` +Если PowerShell не даёт активировать окружение — запустите один раз: +`Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. + +### 🍎 macOS + +```bash +cd hh-resume-booster +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +playwright install chromium +``` +(Если нет питона: `brew install python`.) + +--- + +## 3. Логин (создаём сессию) + +```bash +python login.py # Windows: python login.py | Linux/mac: python login.py +``` +Откроется настоящий браузер. **Войдите в hh.ru** любым способом (почта/пароль, код +из письма/смс, госуслуги). Скрипт сам поймает вход и сохранит `session/storage.json`, +после чего окно можно закрыть. Появится `✅ Готово`. + +> **Сервер без экрана (headless Linux)?** `login.py` требует графику. Запустите его +> на своём компьютере, затем скопируйте получившийся `session/storage.json` на сервер +> в такую же папку `session/`. + +--- + +## 4. Пробный прогон вручную + +```bash +python bump_resumes.py +``` +Ожидаемый вывод — по строке на резюме, например: +``` +[2026-08-24 20:08] 30ee2ebd…: about-added-ok | поднято✓ (next-manual=00:08) +``` +`next-manual=HH:MM` = метка «Можно сегодня в …» на странице резюме; она равна +времени_правки + 4ч и **двигается при каждом прогоне** — это и есть подтверждение, +что резюме подняли. Всё пишется в `bump.log`. + +--- + +## 5. Автозапуск каждые 2 часа + +### 🍎 macOS — launchd + +```bash +bash scheduler/install-mac.sh +``` +- Статус: `launchctl list | grep hhbump` +- Остановить: `launchctl bootout gui/$(id -u)/com.stackops.hhbump` + +### 🐧 Linux — systemd (user timer) + +```bash +bash scheduler/install-linux.sh +``` +- Статус: `systemctl --user list-timers | grep hhbump` +- Разовый прогон: `systemctl --user start hhbump.service` +- Остановить: `systemctl --user disable --now hhbump.timer` + +
Нет systemd? Вариант через cron + +```bash +crontab -e +# добавить строку (замените /ПОЛНЫЙ/ПУТЬ): +0 */2 * * * /ПОЛНЫЙ/ПУТЬ/hh-resume-booster/scheduler/run.sh >> /ПОЛНЫЙ/ПУТЬ/hh-resume-booster/bump.cron.log 2>&1 +``` +(сделайте `chmod +x scheduler/run.sh`) +
+ +### 🪟 Windows — Планировщик заданий + +В PowerShell **из папки пакета**: +```powershell +powershell -ExecutionPolicy Bypass -File .\scheduler\install-windows.ps1 +``` +- Статус: `schtasks /Query /TN hhbump` +- Разовый прогон: `schtasks /Run /TN hhbump` +- Удалить: `schtasks /Delete /TN hhbump /F` + +--- + +## 6. Настройка (необязательно) + +Скопируйте `config.example.json` → `config.json`: + +```json +{ + "resume_ids": ["30ee2ebd...", "fb175217..."], + "headless": true +} +``` +- `resume_ids` — если хотите поднимать только конкретные резюме (иначе — все на аккаунте). + RID виден в URL резюме: `hh.ru/resume/ЭТОТ_ID`. +- `headless: false` — показывать браузер во время прогона (для отладки). + +**Сменить частоту:** поменяйте `StartInterval` (секунды) в plist / `OnUnitActiveSec` +в `hhbump.timer` / интервал в задаче Windows и переустановите соответствующим +`install-*` скриптом. + +--- + +## 7. Диагностика + +| Симптом в `bump.log` | Что делать | +|---|---| +| `STOP: нет session/storage.json` | запустите `python login.py` | +| `STOP: сессия мертва (hhrole=...)` | сессия протухла (живёт ~30 дней) → `python login.py` заново | +| `empty-about-skip` | добавьте текст в блок «О себе» этого резюме | +| `no-about-field` / `ERR` | hh поменяли вёрстку или таймаут — проверьте вручную | + +**Важно:** прогон случается, только когда компьютер **не спит**. launchd/systemd +схлопывают пропуски в один прогон при пробуждении (свежак к утру — это ок). +Нужен режим 24/7 без пропусков — держите машину бодрой или поставьте на VPS. + +--- + +## Состав пакета + +``` +hh-resume-booster/ +├── bump_resumes.py # сам бустер (автоопределение резюме) +├── login.py # логин в hh -> session/storage.json +├── requirements.txt +├── config.example.json +├── README.md +├── session/ # сюда ляжет storage.json (в архиве пусто) +└── scheduler/ + ├── run.sh / run.bat # обёртки для планировщиков + ├── install-mac.sh # launchd + ├── install-linux.sh # systemd user-timer + ├── install-windows.ps1 # Task Scheduler + ├── com.stackops.hhbump.plist # шаблон launchd + ├── hhbump.service / .timer # шаблоны systemd +``` diff --git a/bump_resumes.py b/bump_resumes.py new file mode 100644 index 0000000..659428d --- /dev/null +++ b/bump_resumes.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +hh resume booster — бесплатное «поднятие» резюме в выдаче hh.ru. + +Как это работает: на hh ЛЮБОЕ сохранение резюме заново поднимает его в поиске, +причём БЕЗ 4-часового кулдауна ручной кнопки «Поднять в поиске». Скрипт вносит +невидимую правку в блок «О себе» (тумблерит один двойной пробел в середине текста — +в вёрстке он схлопывается, глазами не виден) и сохраняет. Это бесплатный аналог +платного «Автоподнятия». + +Резюме определяются автоматически из вашей сессии (можно переопределить в config.json). +Запускать по расписанию (launchd / systemd / Task Scheduler) — см. README.md. + +Требуется: session/storage.json (создаётся через `python login.py`). +""" +import asyncio, json, re, datetime, sys +from pathlib import Path +from playwright.async_api import async_playwright + +BASE = Path(__file__).resolve().parent +SESSION = BASE / "session" / "storage.json" +CONFIG = BASE / "config.json" +LOG = BASE / "bump.log" +UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36") + +def cfg(): + if CONFIG.exists(): + try: return json.loads(CONFIG.read_text(encoding="utf-8")) + except Exception: pass + return {} + +def toggle_space(text): + """Невидимый mid-text тумблер: убрать первый двойной пробел ИЛИ добавить его + после первого пробела за 15-м символом (не старт и не конец текста).""" + if " " in text: + return text.replace(" ", " ", 1), "removed" + m = re.search(r'\S \S', text[15:]) + if m: + pos = 15 + m.start() + 1 + return text[:pos] + " " + text[pos:], "added" + return None, "empty-skip" # пустой/слишком короткий «О себе» — не трогаем + +async def discover_rids(pg): + await pg.goto("https://hh.ru/applicant/resumes", wait_until="domcontentloaded", timeout=60000) + await pg.wait_for_timeout(2500) + html = await pg.content() + # RID = 38 hex; /resume/edit/... не матчится (после /resume/ идёт 'edit') + return list(dict.fromkeys(re.findall(r'/resume/([0-9a-f]{30,})', html))) + +async def boost_one(pg, rid): + # 1) правка «О себе» = поднятие + await pg.goto(f"https://hh.ru/resume/edit/{rid}/about", wait_until="domcontentloaded", timeout=60000) + await pg.wait_for_timeout(2200) + ta = pg.locator('[data-qa="resume-editor-about"], textarea[data-qa*="about"], textarea').first + if await ta.count() == 0: + return "no-about-field" + cur = await ta.input_value() + new, how = toggle_space(cur) + if new is None: + return "empty-about-skip (добавьте текст в «О себе»)" + await ta.click(); await ta.fill(new); await pg.wait_for_timeout(500) + saved = False + for sel in ['[data-qa="resume-partial-edit-save"]', 'button:has-text("Сохранить")']: + l = pg.locator(sel).first + if await l.count() > 0 and await l.is_visible(): + await l.click(); saved = True; break + await pg.wait_for_timeout(3000) + ok = saved and f"/resume/{rid}" in pg.url and "edit" not in pg.url + # 2) подтверждение: метка «Можно сегодня в HH:MM» = время правки + 4ч (двигается => подняли) + await pg.goto(f"https://hh.ru/resume/{rid}", wait_until="domcontentloaded", timeout=60000) + await pg.wait_for_timeout(2000) + body = await pg.inner_text("body") + m = re.search(r'[Мм]ожно[^0-9]{0,25}(\d\d:\d\d)', body) + tag = f"next-manual={m.group(1)}" if m else "no-timer-label" + return f"about-{how}-{'ok' if ok else '?'} | поднято✓ ({tag})" + +async def main(): + if not SESSION.exists(): + print("STOP: нет session/storage.json — сначала запустите `python login.py`") + sys.exit(2) + conf = cfg() + ss = json.loads(SESSION.read_text(encoding="utf-8")) + out = [] + async with async_playwright() as p: + b = await p.chromium.launch(headless=conf.get("headless", True), args=["--no-sandbox"]) + ctx = await b.new_context(storage_state=ss, user_agent=UA, locale="ru-RU", + timezone_id="Europe/Moscow", viewport={"width": 1440, "height": 1200}) + pg = await ctx.new_page() + role = {c["name"]: c["value"] for c in await ctx.cookies("https://hh.ru")}.get("hhrole") + if role != "applicant": + out.append(f"STOP: сессия мертва (hhrole={role}) — перелогиньтесь: python login.py") + else: + rids = conf.get("resume_ids") or await discover_rids(pg) + if not rids: + out.append("STOP: резюме не найдены на аккаунте") + for rid in rids: + try: + r = await boost_one(pg, rid) + out.append(f"{rid[:8]}…: {r}") + except Exception as e: + out.append(f"{rid[:8]}…: ERR {str(e)[:70]}") + await b.close() + ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") + line = f"[{ts}] " + " || ".join(out) + print(line) + try: + with LOG.open("a", encoding="utf-8") as fh: + fh.write(line + "\n") + except Exception: + pass + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..8fb3808 --- /dev/null +++ b/config.example.json @@ -0,0 +1,5 @@ +{ + "_comment": "Скопируйте в config.json, если нужно переопределить. Всё опционально.", + "resume_ids": [], + "headless": true +} diff --git a/hh-resume-booster.zip b/hh-resume-booster.zip new file mode 100644 index 0000000000000000000000000000000000000000..da2ea41b3d3acc0e9f5d7f1c210b12b4fcb1757b GIT binary patch literal 14310 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 Y``>UNAivu|1%4O+Wt=1.40 diff --git a/scheduler/com.stackops.hhbump.plist b/scheduler/com.stackops.hhbump.plist new file mode 100644 index 0000000..a2ec110 --- /dev/null +++ b/scheduler/com.stackops.hhbump.plist @@ -0,0 +1,17 @@ + + + + + Label com.stackops.hhbump + ProgramArguments + + __DIR__/.venv/bin/python + __DIR__/bump_resumes.py + + StartInterval 7200 + RunAtLoad + StandardOutPath __DIR__/bump.out.log + StandardErrorPath__DIR__/bump.err.log + ProcessType Background + + diff --git a/scheduler/hhbump.service b/scheduler/hhbump.service new file mode 100644 index 0000000..54023b0 --- /dev/null +++ b/scheduler/hhbump.service @@ -0,0 +1,7 @@ +[Unit] +Description=hh resume booster (одноразовый прогон) + +[Service] +Type=oneshot +WorkingDirectory=__DIR__ +ExecStart=__DIR__/.venv/bin/python __DIR__/bump_resumes.py diff --git a/scheduler/hhbump.timer b/scheduler/hhbump.timer new file mode 100644 index 0000000..dbeeb75 --- /dev/null +++ b/scheduler/hhbump.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Запуск hh resume booster каждые 2 часа + +[Timer] +OnBootSec=5min +OnUnitActiveSec=2h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/scheduler/install-linux.sh b/scheduler/install-linux.sh new file mode 100644 index 0000000..ce3bb50 --- /dev/null +++ b/scheduler/install-linux.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Linux: ставит systemd user-timer (каждые 2ч) с автоподстановкой путей. +set -e +DIR="$(cd "$(dirname "$0")/.." && pwd)" +mkdir -p "$HOME/.config/systemd/user" +sed "s#__DIR__#$DIR#g" "$DIR/scheduler/hhbump.service" > "$HOME/.config/systemd/user/hhbump.service" +cp "$DIR/scheduler/hhbump.timer" "$HOME/.config/systemd/user/hhbump.timer" +systemctl --user daemon-reload +systemctl --user enable --now hhbump.timer +# чтобы таймер работал и когда вы не залогинены в графическую сессию: +loginctl enable-linger "$USER" 2>/dev/null || true +echo "✅ Установлено. Проверка: systemctl --user list-timers | grep hhbump" +echo " Разовый прогон сейчас: systemctl --user start hhbump.service" +echo " Остановить: systemctl --user disable --now hhbump.timer" +echo " Логи: $DIR/bump.log (и: journalctl --user -u hhbump.service)" diff --git a/scheduler/install-mac.sh b/scheduler/install-mac.sh new file mode 100644 index 0000000..5716648 --- /dev/null +++ b/scheduler/install-mac.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# macOS: ставит launchd-агент (каждые 2ч) с автоподстановкой путей. +set -e +DIR="$(cd "$(dirname "$0")/.." && pwd)" +PLIST="$HOME/Library/LaunchAgents/com.stackops.hhbump.plist" +mkdir -p "$HOME/Library/LaunchAgents" +sed "s#__DIR__#$DIR#g" "$DIR/scheduler/com.stackops.hhbump.plist" > "$PLIST" +launchctl bootout "gui/$(id -u)/com.stackops.hhbump" 2>/dev/null || true +launchctl bootstrap "gui/$(id -u)" "$PLIST" +echo "✅ Установлено. Проверка: launchctl list | grep hhbump" +echo " Остановить: launchctl bootout gui/\$(id -u)/com.stackops.hhbump" +echo " Логи: $DIR/bump.log" diff --git a/scheduler/install-windows.ps1 b/scheduler/install-windows.ps1 new file mode 100644 index 0000000..38fb9c0 --- /dev/null +++ b/scheduler/install-windows.ps1 @@ -0,0 +1,16 @@ +# Windows: регистрирует задачу в Планировщике (каждые 2 часа). +# Запуск: правой кнопкой -> "Выполнить с помощью PowerShell", ИЛИ в PowerShell: +# powershell -ExecutionPolicy Bypass -File .\scheduler\install-windows.ps1 +$Root = (Resolve-Path "$PSScriptRoot\..").Path +$Bat = Join-Path $Root "scheduler\run.bat" +$Action = New-ScheduledTaskAction -Execute $Bat +$Trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) ` + -RepetitionInterval (New-TimeSpan -Hours 2) -RepetitionDuration ([TimeSpan]::MaxValue) +$Settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries +Register-ScheduledTask -TaskName "hhbump" -Action $Action -Trigger $Trigger ` + -Settings $Settings -Description "hh resume booster" -Force +Write-Host "OK. Проверка: schtasks /Query /TN hhbump" +Write-Host "Разовый прогон: schtasks /Run /TN hhbump" +Write-Host "Удалить: schtasks /Delete /TN hhbump /F" +Write-Host "Логи: $Root\bump.log" diff --git a/scheduler/run.bat b/scheduler/run.bat new file mode 100644 index 0000000..a7b0b3b --- /dev/null +++ b/scheduler/run.bat @@ -0,0 +1,4 @@ +@echo off +REM Обёртка для Планировщика заданий Windows. +cd /d "%~dp0.." +".venv\Scripts\python.exe" bump_resumes.py diff --git a/scheduler/run.sh b/scheduler/run.sh new file mode 100755 index 0000000..fcfaaec --- /dev/null +++ b/scheduler/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Обёртка для планировщиков (launchd/cron/systemd) — идёт в корень пакета и запускает бустер. +cd "$(dirname "$0")/.." || exit 1 +exec ./.venv/bin/python bump_resumes.py