ГлавнаяБлогКак я создал Python-бота для поиска работы и автоматизировал собеседования
Python

Как я создал Python-бота для поиска работы и автоматизировал собеседования

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

Al
Редакция Algolitalgolit.ru
12 мин чтения22 июля 2026 г.

Зачем автоматизировать поиск работы?

Вы тратите часы на просмотр досок вакансий, но большинство предложений не подходят? Я столкнулся с этим, когда искал работу в Амстердаме. После 73 заявок и 61 игнора я понял: нужно автоматизировать процесс. В этой статье я расскажу, как написал Python-скрипт, который собирает вакансии, фильтрует их по моим критериям и присылает топ-3 в Telegram каждое утро. Вы сможете адаптировать код под себя и сэкономить десятки часов.

Проблемы поиска работы в IT

Когда я решил переехать в Амстердам и искать позиции Platform Engineer, я столкнулся с тремя проблемами:

  • Шум: 90% вакансий с тегом «Cloud Engineer» — это поддержка, а не разработка. Нужен фильтр под мой стек: Go, Kubernetes, Cilium, eBPF.
  • Время: Лучшие роли в Adyen, Booking.com появляются во вторник утром, а к четвергу — 200 откликов. Я проверял доски в воскресенье — всегда опаздывал.
  • Контекст: Каждая проверка LinkedIn разрушала поток. Я хотел один дайджест в 7 утра, прочитать в поезде и откликнуться на топ-3.

Архитектура бота

Вот как устроен пайплайн:

┌─────────────┐    ┌──────────────┐    ┌──────────────┐    ┌─────────────┐
│  Парсеры    │───►│  Обогащение  │───►│  Ранжирование│───►│  Уведомление│
│ (async)     │    │ (LLM + правила)│    │ (оценка)    │    │ (Telegram)  │
└─────────────┘    └──────────────┘    └──────────────┘    └─────────────┘
     │                                              │
     ▼                                              ▼
 LinkedIn Jobs                              SQLite кэш
 Indeed                                     (дедупликация)
 Glassdoor
 We Work Remotely

Ключевые решения

  • Язык: Python 3.11 + asyncio — быстрая разработка, экосистема BeautifulSoup/httpx.
  • Хранилище: SQLite — без инфраструктуры, портативно, WAL для конкурентного чтения.
  • Парсинг: httpx + BeautifulSoup — легко, дёшево. Для JS-сайтов — fallback на headless браузер.
  • Лимиты: Адаптивный backoff (экспоненциальный + jitter) — уважает robots.txt.
  • LLM: OpenAI GPT-4o-mini — ~$0.02 за 100 вакансий. Качество оправдывает цену.
  • Доставка: Telegram Bot API — я живу в Telegram, push-уведомления.
  • Запуск: cron на Raspberry Pi — бесплатно, постоянно.

Код: пайплайн шаг за шагом

1. Сбор данных

import httpx
from bs4 import BeautifulSoup
from dataclasses import dataclass

@dataclass
class RawJob:
    title: str
    company: str
    location: str
    description: str
    salary: str | None
    url: str
    posted_at: str

async def scrape_linkedin(query: str, location: str) -> list[RawJob]:
    """Парсит LinkedIn Jobs через JSON endpoint."""
    url = f"https://www.linkedin.com/jobs-guest/jobs/api/jobs?keywords={query}&location={location}"
    headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}
    async with httpx.AsyncClient() as client:
        resp = await client.get(url, headers=headers, timeout=30)
        soup = BeautifulSoup(resp.text, "html.parser")
        jobs = []
        for li in soup.select(".job-result-card"):
            title = li.select_one(".job-result-card__title").text.strip()
            company = li.select_one(".job-result-card__company-name").text.strip()
            location = li.select_one(".job-result-card__location").text.strip()
            url = li.select_one("a")["href"]
            jobs.append(RawJob(title, company, location, "", None, url, ""))
        return jobs

2. Нормализация

@dataclass
class Job:
    title: str
    company: str
    location: str
    seniority: str  # Junior, Mid, Senior, Staff
    remote: str     # onsite, hybrid, remote
    salary_min: int | None
    salary_max: int | None
    description: str
    url: str
    posted_at: str

def normalize(raw: RawJob) -> Job:
    """Приводит данные к единой схеме."""
    # Определяем уровень
    title_lower = raw.title.lower()
    if "junior" in title_lower:
        seniority = "Junior"
    elif "senior" in title_lower or "staff" in title_lower:
        seniority = "Senior"
    elif "principal" in title_lower:
        seniority = "Principal"
    else:
        seniority = "Mid"
    # Парсим зарплату из описания (упрощённо)
    salary = parse_salary(raw.description)
    return Job(
        title=raw.title,
        company=raw.company,
        location=raw.location.split(",")[0],  # "Amsterdam, North Holland" → "Amsterdam"
        seniority=seniority,
        remote="hybrid",  # заглушка
        salary_min=salary[0] if salary else None,
        salary_max=salary[1] if salary else None,
        description=raw.description,
        url=raw.url,
        posted_at=raw.posted_at
    )

3. Фильтрация

TARGET_CITIES = {"Amsterdam", "Berlin", "Dublin"}
TECH_KEYWORDS = ["go", "kubernetes", "k8s", "cilium", "ebpf", "terraform", "distributed systems"]
BLOCKLIST = {"Randstad", "Hays", "Michael Page"}

def passes_filter(job: Job) -> bool:
    """Жёсткие правила: локация, уровень, технологии, чёрный список."""
    return (
        job.location in TARGET_CITIES
        and job.seniority in {"Senior", "Staff", "Principal"}
        and any(kw in job.description.lower() for kw in TECH_KEYWORDS)
        and job.company not in BLOCKLIST
    )

4. Обогащение через LLM

import openai

@dataclass
class EnrichedJob(Job):
    llm_score: float
    verdict: str  # yes, no, maybe

def enrich_with_llm(job: Job) -> EnrichedJob:
    """LLM оценивает соответствие вакансии профилю."""
    prompt = f"""
Роль: {job.title} в {job.company}
Описание: {job.description[:2000]}

Профиль кандидата: 6 лет в облаке/platform, K8s (Cilium, Talos),
изучает Go, Azure+AWS, комплаенс (NIS2/DORA).
Цель: Platform Engineer или Backend Engineer в Амстердаме/Берлине/Дублине.

Задачи:
1. Это действительно backend/platform работа (не поддержка, не MSP)?
2. В стеке есть K8s, Go, распределённые системы или cloud-native?
3. Вероятен ли спонсор визы? (большие компании — да, маленькие — нет)
4. Оценка соответствия от 0 до 100.
5. Стоит ли откликаться? yes/no/maybe.

Ответ в формате JSON: {{"score": число, "verdict": "yes/no/maybe", "is_backend": bool, "tech_match": bool, "visa_likely": bool}}
"""
    response = openai.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    result = json.loads(response.choices[0].message.content)
    return EnrichedJob(
        **job.__dict__,
        llm_score=result["score"],
        verdict=result["verdict"]
    )

5. Ранжирование

def score(job: EnrichedJob) -> float:
    """Композитная оценка: LLM (50%), зарплата (20%), компания (20%), свежесть (10%)."""
    return (
        job.llm_score * 0.5
        + salary_score(job.salary_max) * 0.2
        + company_tier_score(job.company) * 0.2
        + recency_score(job.posted_at) * 0.1
    )

def salary_score(max_salary: int | None) -> float:
    if max_salary is None:
        return 0.5
    if max_salary >= 100000:
        return 1.0
    elif max_salary >= 80000:
        return 0.8
    else:
        return 0.5

def company_tier_score(company: str) -> float:
    TIER1 = {"Adyen", "Booking.com", "ASML", "Uber", "Spotify"}
    return 1.0 if company in TIER1 else 0.5

def recency_score(posted_at: str) -> float:
    # Упрощённо: чем свежее, тем выше
    return 1.0  # заглушка

6. Отправка в Telegram

import asyncio
from telegram import Bot

TELEGRAM_TOKEN = "ваш_токен"
CHAT_ID = "ваш_chat_id"

async def send_digest(jobs: list[EnrichedJob]):
    """Отправляет топ-3 вакансий в Telegram."""
    bot = Bot(token=TELEGRAM_TOKEN)
    top3 = sorted(jobs, key=score, reverse=True)[:3]
    msg = "🔍 Ежедневный дайджест вакансий\n"
    for i, job in enumerate(top3, 1):
        msg += f"\n{i}. {job.title} — {job.company}\n   Локация: {job.location}\n   Оценка: {job.llm_score:.0f}/100\n   {job.url}"
    await bot.send_message(chat_id=CHAT_ID, text=msg)

# Запуск
asyncio.run(send_digest(enriched_jobs))

5 ошибок, которые я совершил

1. LinkedIn заблокировал меня через 6 минут

Я использовал один сеанс httpx с задержкой 2 секунды. LinkedIn вернул 429 после ~30 запросов. Решение: парсить JSON endpoint /jobs-guest/jobs/api/jobs вместо HTML. Он стабильнее. Также я убрал прокси и просто соблюдал паузу 5 секунд.

2. LLM придумала вакансию в Google

GPT-4o-mini выдумал роль «Kubernetes Platform Engineer» с зарплатой €500K. URL не существовал. Решение: перед обогащением проверять URL через HEAD-запрос. Если статус не 200 — отбрасывать.

3. Я откликнулся на одну вакансию 4 раза

Ключ (title, company) не работал, когда компания перепубликовала вакансию с другим названием. Решение: хэшировать описание после удаления пробелов и дат. Одинаковое описание = одна вакансия.

4. Скрипт умирал при закрытии ноутбука

Я запускал python main.py вручную и забывал. Решение: запустить на Raspberry Pi через cron. Pi работает 24/7, SQLite на внешнем SSD.

5. 18 «хороших» вакансий парализовали

Я не откликался ни на одну, потому что не мог выбрать. Решение: показывать только топ-3. Остальные по команде /more. Это увеличило количество откликов.

Результаты

  • Время на поиск: с 45 минут в день до 5 минут на чтение дайджеста.
  • Откликов в неделю: с 8 до 12 (регулярно).
  • Соответствие стеку: с 30% до 85%.
  • Время реакции: с 3-4 дней до <24 часов.
  • Релевантные собеседования: с 1-2 до 4-5 в месяц.

Главное — я перестал тратить вечера на поиск. Система работает без меня.

Как начать прямо сейчас

  1. Склонируйте репозиторий: git clone https://github.com/beltagyy/job-digest.git
  2. Установите зависимости: pip install -r requirements.txt
  3. Настройте config.yaml: добавьте OpenAI API ключ, Telegram токен, города и ключевые слова.
  4. Запустите: python main.py --run-now
  5. Настройте cron на сервере для ежедневного запуска.

Код открыт, и я жду ваших пул-реквестов. Добавьте парсер для AngelList или Hacker News — и помогите сообществу. Удачи в поиске!

#Python#автоматизация#поиск работы#парсинг#Telegram бот
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →