August 19

Бот, который откликается на вакансии за меня

После истории со скриптом, который вылавливал вакансии в телеграм-каналах, я подумал: а почему бы не пойти дальше? На hh.ru та же самая проблема, только в других декорациях — свежие вакансии по 3D и моушену появляются, откликов набирается уже полсотни в первые полчаса, и если ты не в числе первых десяти, шансы просесть очень сильно. Ручками сидеть и обновлять поиск каждые пять минут — это не жизнь. Так родился второй скрипт, который уже не просто присылает уведомление, а сам заходит на сайт, читает вакансию, решает, стоит ли откликаться, и если да — откликается вместо меня, с сопроводительным письмом.

Из чего это собрано

В основе — Playwright, библиотека для управления браузером из кода. По сути это тот же браузер, что открывается у тебя на компьютере, только им управляет скрипт: он умеет кликать, скроллить, читать текст со страницы, заполнять формы. Я специально запускаю его не в headless-режиме, а с видимым окном — так проще заметить, если что-то пошло не так, и вручную вмешаться.

Для распознавания «подходит вакансия или нет» используется та же связка, что и в телеграм-версии — локальная модель через Ollama. Ключевые слова и правила фильтрации вынесены в отдельный файл filters.txt, чтобы не лезть в код каждый раз, когда нужно подправить список профессий. Модель получает текст вакансии и по инструкции решает: да, подходит, вот название должности — или нет.

Как проходит один цикл

Скрипт открывает поиск на hh.ru с готовыми параметрами — по сути, обычная страница результатов, только с сортировкой по свежести и периодом в один день. Дальше он прокручивает страницу вниз несколько раз, чтобы подгрузились все вакансии, и собирает ссылки на каждую карточку.

По каждой ссылке скрипт заходит на страницу вакансии по очереди. Первым делом проверяет, не откликался ли уже на эту вакансию раньше — для этого ведётся простой текстовый журнал applied_links.log, куда пишутся уже пройденные ссылки без параметров запроса (чтобы одна и та же вакансия с разными UTM-метками не считалась дважды).

Дальше идёт проверка на «вакансию с тестом» — на hh.ru часто вместо обычного отклика просят сначала пройти тестовое задание или ответить на вопросы прямо на странице. Такие вакансии скрипт узнаёт по характерным фразам в описании вроде «пройти тест» или «заполните анкету», а также по слову question в самой ссылке. Если это тестовая вакансия — скрипт помечает её как обработанную, но вкладку намеренно не закрывает, чтобы я потом мог сам вручную пройти тест, если вакансия того стоит. Автоматизировать прохождение произвольных тестов — идея так себе, там слишком много нестандартного.

Если тестов нет, весь текст страницы отправляется в модель на вердикт. Если ответ отрицательный, вакансия просто помечается пройденной и скрипт идёт дальше. Если положительный — начинается самое интересное.

Автоматический отклик

Скрипт ищет кнопку отклика на странице, кликает по ней, и дальше по цепочке разбирает возможные всплывающие окна: предупреждение про переезд в другой город («Всё равно откликнуться»), выбор резюме из списка, если их несколько, поле для сопроводительного письма. Письмо берётся не из головы модели, а из заранее заготовленных шаблонов в файле templates.txt — они чередуются по кругу через itertools.cycle, чтобы не заваливать разных работодателей одинаковым текстом слово в слово. После заполнения скрипт нажимает «Отправить» и записывает результат.

Каждый успешный отклик попадает в Excel-таблицу — через pandas дописывается строка с названием вакансии, компанией, ссылкой, статусом и датой. Получается что-то вроде личного трекера откликов, который ведётся сам, без единой лишней вкладки Google Sheets.

Мелочи, которые оказались важны на практике

Часть решений в коде — это не архитектурные изыски, а просто следы набитых шишек. Например, отдельно оговорено, что текст вакансии для проверки на «тест» берётся именно из блока описания, а не со всей страницы целиком — потому что в шапке или подвале сайта тоже может случайно встретиться слово вроде «тестирование», и тогда скрипт зря пропускал бы нормальную вакансию. Ещё один нюанс — переменные окружения NO_PROXY и all_proxy выставляются в начале файла, чтобы запросы к локальной Ollama не улетали через прокси, который используется для остального трафика.

В конце работы, после того как список вакансий пройден, скрипт не завершается сразу — он ждёт, пока я не закрою вручную все оставшиеся вкладки с тестовыми заданиями. Это осознанный компромисс: полностью бросать открытые вкладки не хочется, а автоматически их закрывать — значит потерять те вакансии, которые могли бы того стоить.

В сумме получилось не идеальное, но вполне рабочее решение: я просыпаюсь, а часть вакансий, на которые стоило откликнуться, уже обработана, письмо отправлено, запись в таблице есть, и в браузере меня ждут только те случаи, где действительно нужна моя голова, а не набор правил.