Alerta de Vagas Python com Telegram, RSS e GitHub Actions

Aprenda a criar um alerta de vagas Python com feed RSS, filtros, deduplicação, Telegram e GitHub Actions sem expor tokens no repositório.

7 min de leitura Equipe Python Brasil

Acompanhar vagas Python manualmente é cansativo. Você abre LinkedIn, comunidades, páginas de empresas, agregadores, grupos de Telegram e a seção de vagas Python quando lembra. No fim, oportunidades boas passam porque a rotina depende de atenção diária. Um alerta simples resolve parte desse problema e ainda vira um projeto de portfólio excelente: pequeno, útil, integrável e fácil de explicar em entrevista.

Neste guia, vamos montar um alerta de vagas Python com Telegram, usando feed RSS ou fonte pública permitida, filtros por palavra-chave, deduplicação local e execução agendada com GitHub Actions. O objetivo não é criar um crawler agressivo nem copiar base de terceiros. A ideia é construir uma automação responsável que monitora poucas fontes, avisa quando aparece algo relevante e deixa rastros suficientes para manutenção.

Se você está estudando para o mercado, este projeto conversa com projetos de portfólio Python, primeiro emprego como programador Python, HTTPX com timeouts e retries e variáveis de ambiente com python-dotenv. Para quem já acompanha vagas de backend em outras linguagens, vale comparar com oportunidades em Golang Brasil.

O que o projeto vai fazer

O fluxo mínimo tem cinco etapas:

  1. buscar itens de um feed RSS ou endpoint público permitido;
  2. filtrar títulos e descrições por termos como python, django, fastapi, dados, júnior e remoto;
  3. comparar cada vaga com um arquivo local de IDs já enviados;
  4. mandar mensagem para um chat do Telegram;
  5. salvar o estado para não repetir o mesmo alerta.

Essa arquitetura é pequena, mas demonstra várias habilidades profissionais. Você precisa lidar com rede instável, variável de ambiente, parsing de dados, persistência leve, idempotência, logs e agendamento. Em entrevista, isso rende uma conversa melhor do que “fiz um bot que imprime mensagem”.

Cuidados antes de coletar vagas

Automação de vagas exige bom senso. Nem todo site permite scraping. Mesmo quando a página é pública, isso não significa que você pode fazer requisições agressivas, republicar conteúdo integral ou ignorar termos de uso.

Boas regras para portfólio:

  • prefira RSS, API pública, exportação permitida ou página própria;
  • faça poucas requisições por execução;
  • use timeout e user-agent claro;
  • salve cache para não bater na mesma fonte toda hora;
  • envie apenas título, empresa quando disponível, URL e um resumo curto;
  • não colete dados pessoais desnecessários;
  • documente as fontes e limites no README.

Para este tutorial, vamos usar uma função genérica que lê RSS. Você pode apontar para uma fonte permitida no seu ambiente ou adaptar para uma lista local de exemplos. Se quiser usar a página de vagas do próprio Python Brasil em um projeto pessoal, trate como fonte para estudo e não como desculpa para martelar o servidor.

Estrutura do projeto

Crie uma pasta simples:

alerta-vagas-python/
  alerta_vagas.py
  requirements.txt
  .env.example
  sent_jobs.json
  tests/
    test_filters.py
  .github/
    workflows/
      alerta.yml

O sent_jobs.json pode começar vazio:

[]

Em projeto real, ele poderia virar SQLite, Redis ou uma tabela em PostgreSQL. Para portfólio júnior, JSON é suficiente se você explicar o limite: funciona para volume pequeno e execução serial.

Dependências

Use poucas bibliotecas:

feedparser==6.0.11
httpx==0.27.2
python-dotenv==1.0.1

Instale em ambiente virtual:

python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

O feedparser simplifica RSS. O httpx envia mensagens para a Bot API do Telegram com timeout explícito. O python-dotenv ajuda no desenvolvimento local, mas em CI o ideal é usar secrets da plataforma.

Configuração sem vazar segredo

Crie .env.example assim:

TELEGRAM_BOT_TOKEN=coloque_o_token_aqui
TELEGRAM_CHAT_ID=coloque_o_chat_id_aqui
JOBS_FEED_URL=https://exemplo.com/feed.xml
KEYWORDS=python,django,fastapi,dados,junior,júnior,remoto

Não versione o .env real. No GitHub Actions, salve TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID e JOBS_FEED_URL como secrets ou variables. Se usar Gitea Actions, aplique o mesmo princípio: segredo fica na plataforma, não no Git.

Código do alerta

Um primeiro script completo pode ser assim:

from __future__ import annotations

import hashlib
import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path

import feedparser
import httpx
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
STATE_FILE = Path("sent_jobs.json")


@dataclass(frozen=True)
class JobItem:
    id: str
    title: str
    link: str
    summary: str


def required_env(name: str) -> str:
    value = os.getenv(name)
    if not value:
        raise RuntimeError(f"Variável obrigatória ausente: {name}")
    return value


def load_sent_ids() -> set[str]:
    if not STATE_FILE.exists():
        return set()
    return set(json.loads(STATE_FILE.read_text(encoding="utf-8")))


def save_sent_ids(ids: set[str]) -> None:
    STATE_FILE.write_text(
        json.dumps(sorted(ids), ensure_ascii=False, indent=2),
        encoding="utf-8",
    )


def stable_id(title: str, link: str) -> str:
    raw = f"{title}|{link}".encode("utf-8")
    return hashlib.sha256(raw).hexdigest()[:16]


def parse_feed(feed_url: str) -> list[JobItem]:
    parsed = feedparser.parse(feed_url)
    items: list[JobItem] = []

    for entry in parsed.entries[:30]:
        title = getattr(entry, "title", "").strip()
        link = getattr(entry, "link", "").strip()
        summary = getattr(entry, "summary", "").strip()
        if not title or not link:
            continue
        items.append(JobItem(stable_id(title, link), title, link, summary))

    return items


def matches_keywords(item: JobItem, keywords: list[str]) -> bool:
    text = f"{item.title} {item.summary}".casefold()
    return any(keyword.casefold().strip() in text for keyword in keywords if keyword.strip())


def format_message(item: JobItem) -> str:
    return f"Nova vaga Python encontrada:\n\n{item.title}\n{item.link}"


def send_telegram(token: str, chat_id: str, text: str) -> None:
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = {"chat_id": chat_id, "text": text, "disable_web_page_preview": True}
    with httpx.Client(timeout=10.0) as client:
        response = client.post(url, json=payload)
        response.raise_for_status()


def main() -> None:
    feed_url = required_env("JOBS_FEED_URL")
    token = required_env("TELEGRAM_BOT_TOKEN")
    chat_id = required_env("TELEGRAM_CHAT_ID")
    keywords = os.getenv("KEYWORDS", "python,fastapi,django").split(",")

    sent_ids = load_sent_ids()
    candidates = [item for item in parse_feed(feed_url) if matches_keywords(item, keywords)]
    new_items = [item for item in candidates if item.id not in sent_ids]

    logging.info("%s itens candidatos, %s novos", len(candidates), len(new_items))

    for item in new_items[:5]:
        send_telegram(token, chat_id, format_message(item))
        sent_ids.add(item.id)

    save_sent_ids(sent_ids)


if __name__ == "__main__":
    main()

Alguns detalhes importam. required_env falha cedo quando falta segredo. stable_id evita depender de IDs inconsistentes do feed. new_items[:5] limita rajadas caso a fonte publique muitas vagas de uma vez. response.raise_for_status() transforma erro HTTP em falha visível no log.

Testando a lógica de filtro

Não precisa testar o Telegram real em todo commit. Comece testando a parte pura:

from alerta_vagas import JobItem, matches_keywords, stable_id


def test_matches_python_keyword():
    item = JobItem("1", "Dev Python Júnior", "https://exemplo.com", "FastAPI remoto")
    assert matches_keywords(item, ["python", "django"])


def test_does_not_match_unrelated_job():
    item = JobItem("1", "Designer Produto", "https://exemplo.com", "Figma remoto")
    assert not matches_keywords(item, ["python", "fastapi"])


def test_stable_id_repeats_for_same_title_and_link():
    assert stable_id("Dev Python", "https://x.test") == stable_id("Dev Python", "https://x.test")

Rode:

pytest

Para evoluir, use respx ou monkeypatch para simular erro do Telegram. O objetivo é provar que o bot não repete vaga, não manda tudo sem filtro e falha de forma clara quando a configuração está incompleta.

Agendando com GitHub Actions

Um workflow diário pode ficar assim:

name: alerta-vagas-python

on:
  schedule:
    - cron: "0 12 * * *"
  workflow_dispatch:

jobs:
  run:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - run: python alerta_vagas.py
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
          JOBS_FEED_URL: ${{ vars.JOBS_FEED_URL }}
          KEYWORDS: python,django,fastapi,dados,júnior,remoto
      - name: Commit state
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add sent_jobs.json
          git diff --cached --quiet || git commit -m "chore: update sent jobs"
          git push

Esse desenho commita o estado no próprio repositório. É simples, mas tem trade-off: se duas execuções rodarem juntas, pode haver conflito. Para portfólio, documente isso. Para produção, use banco, storage externo ou uma fila com lock.

Melhorias para deixar o portfólio mais forte

Depois do mínimo funcionando, adicione uma melhoria por vez:

  • filtros por senioridade, localidade e modelo remoto;
  • arquivo sources.yml com múltiplos feeds permitidos;
  • mensagem com empresa, salário e tags quando a fonte trouxer esses campos;
  • --dry-run para testar sem enviar Telegram;
  • logs estruturados com quantidade de itens lidos, filtrados e enviados;
  • SQLite para estado quando o volume crescer;
  • testes para fonte vazia, item sem link e erro HTTP;
  • README com print da mensagem e instrução de secrets.

Não coloque dados reais de candidatos, tokens ou dumps de plataformas no repositório. O valor do projeto está na automação e no cuidado operacional, não em copiar o máximo de vagas possível.

Como explicar em entrevista

Uma boa explicação seria:

Criei um alerta de vagas Python que lê uma fonte pública permitida, filtra termos relevantes, deduplica itens já enviados e manda resumo no Telegram. Usei variáveis de ambiente para segredo, timeout em chamadas HTTP, limite de mensagens por execução e testes para a lógica de filtro. O estado fica em JSON por simplicidade; se o volume crescesse, eu migraria para SQLite ou PostgreSQL.

Essa resposta mostra maturidade. Você reconhece limite técnico, explica decisão e conecta o projeto a uso real.

Próximos passos

Se seu objetivo é conseguir vaga, transforme este alerta em um projeto pequeno de GitHub com README caprichado. Depois conecte com um dashboard simples de vagas por tecnologia, usando Pandas ou GeoPandas para analisar localização. Para publicar uma versão mais robusta, combine com Docker para Python, pytest e OpenTelemetry com Python.

O ponto principal é sair da busca manual e construir uma rotina verificável. Mesmo que o alerta encontre poucas vagas, ele prova exatamente o que empresas procuram em automações júnior: problema claro, escopo pequeno, segredo protegido, logs, testes e manutenção possível.

E

Equipe Python Brasil

Contribuidor do Python Brasil — Aprenda Python em Português