Use Cases

Raspagem do Job Board com manipulação de CAPTCHA usando CaptchaAI

Quanto tempo um scraper roda contra o Indeed, o LinkedIn ou o Glassdoor antes de esbarrar num CAPTCHA? Poucas dezenas de requisições fora do padrão humano já bastam — não é falha de configuração, é a defesa padrão desses portais, normalmente via reCAPTCHA v2, reCAPTCHA v3 ou Cloudflare Turnstile.

Este guia monta um scraper de vagas em Python que resolve o CAPTCHA pela API assim que ele aparece e reaproveita a mesma base para levantamento salarial por cargo e região.


Qual CAPTCHA cada portal de vagas usa

Antes de escrever o scraper, vale mapear qual desafio cada portal costuma apresentar — isso define o method certo a passar na chamada de API:

Portal Tipo de CAPTCHA O que aciona o desafio Dados disponíveis
Indeed reCAPTCHA v2 Volume alto de requisições Vagas, faixas salariais
LinkedIn Cloudflare Turnstile Detecção de bots Vagas, dados da empresa
Glassdoor reCAPTCHA v2 Detecção de scraping Avaliações, salários, vagas
ZipRecruiter Cloudflare Turnstile Acesso automatizado Vagas
Monster reCAPTCHA v2 Páginas de busca Vagas
CareerBuilder reCAPTCHA v3 Login, busca Vagas, busca de currículo

Scraper de portal de vagas com resolução de CAPTCHA integrada

A classe abaixo varre as páginas de busca, identifica se o CAPTCHA é reCAPTCHA ou Turnstile pelo indicador presente no HTML, resolve pela API e reenvia a requisição antes de seguir para a próxima página:

import requests
import time
import re
from bs4 import BeautifulSoup

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"


def solve_captcha(method, sitekey, pageurl, **kwargs):
    data = {
        "key": CAPTCHAAI_KEY,
        "method": method,
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1,
    }
    data.update(kwargs)
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data=data)
    task_id = resp.json()["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        r = result.json()
        if r["request"] != "CAPCHA_NOT_READY":
            return r["request"]
    raise TimeoutError("Solve timeout")


class JobBoardScraper:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
            "Accept-Language": "en-US,en;q=0.9",
        })

    def search_jobs(self, base_url, query, location, pages=5):
        """Search job listings across multiple pages."""
        all_jobs = []

        for page in range(pages):
            url = f"{base_url}/jobs?q={query}&l={location}&start={page * 10}"
            resp = self.session.get(url, timeout=30)

            # Check for CAPTCHA
            if self._has_captcha(resp.text):
                resp = self._solve_and_retry(resp.text, url)

            if resp.status_code == 200:
                jobs = self._parse_listings(resp.text)
                all_jobs.extend(jobs)
                print(f"Page {page + 1}: {len(jobs)} jobs found")
            else:
                print(f"Page {page + 1}: Request failed ({resp.status_code})")

            time.sleep(3)  # Rate limit

        return all_jobs

    def _has_captcha(self, html):
        indicators = [
            'data-sitekey=',
            'g-recaptcha',
            'cf-turnstile',
            'captcha-delivery',
        ]
        return any(ind in html.lower() for ind in indicators)

    def _solve_and_retry(self, html, url):
        # Try reCAPTCHA first
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if match:
            sitekey = match.group(1)

            # Detect Turnstile vs reCAPTCHA
            if 'cf-turnstile' in html:
                token = solve_captcha("turnstile", sitekey, url)
                field = "cf-turnstile-response"
            else:
                token = solve_captcha("userrecaptcha", sitekey, url)
                field = "g-recaptcha-response"

            return self.session.post(url, data={field: token})

        return self.session.get(url)

    def _parse_listings(self, html):
        soup = BeautifulSoup(html, "html.parser")
        jobs = []

        for card in soup.select(".job_seen_beacon, .jobsearch-ResultsList > li"):
            title_el = card.select_one("h2 a, .jobTitle a")
            company_el = card.select_one(".companyName, [data-testid='company-name']")
            location_el = card.select_one(".companyLocation, [data-testid='text-location']")
            salary_el = card.select_one(".salary-snippet, .estimated-salary")

            if title_el:
                jobs.append({
                    "title": title_el.get_text(strip=True),
                    "company": company_el.get_text(strip=True) if company_el else "",
                    "location": location_el.get_text(strip=True) if location_el else "",
                    "salary": salary_el.get_text(strip=True) if salary_el else "",
                    "url": title_el.get("href", ""),
                })

        return jobs


# Usage
scraper = JobBoardScraper(
    proxy="http://user:pass@residential.proxy.com:5000"
)
jobs = scraper.search_jobs(
    base_url="https://jobs.example.com",
    query="python developer",
    location="New York",
    pages=10,
)
print(f"Total jobs collected: {len(jobs)}")

Cruzando cargo e localidade para levantamento salarial

Com a mesma classe, dá para varrer combinações de cargo e cidade e consolidar tudo em um CSV para análise:

import csv


def collect_salary_data(titles, locations, output_file):
    """Collect salary data across job titles and locations."""
    scraper = JobBoardScraper(
        proxy="http://user:pass@residential.proxy.com:5000"
    )

    results = []
    for title in titles:
        for location in locations:
            try:
                jobs = scraper.search_jobs(
                    "https://jobs.example.com",
                    title, location, pages=3,
                )
                salaries = [j["salary"] for j in jobs if j["salary"]]
                results.append({
                    "title": title,
                    "location": location,
                    "listings": len(jobs),
                    "with_salary": len(salaries),
                    "salary_samples": "; ".join(salaries[:5]),
                })
                time.sleep(5)
            except Exception as e:
                results.append({
                    "title": title,
                    "location": location,
                    "error": str(e),
                })

    with open(output_file, "w", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=["title", "location", "listings",
                           "with_salary", "salary_samples", "error"],
        )
        writer.writeheader()
        writer.writerows(results)

    return results


# Collect salary data for market analysis
collect_salary_data(
    titles=["Data Engineer", "ML Engineer", "DevOps Engineer"],
    locations=["San Francisco", "New York", "Austin", "Remote"],
    output_file="salary_data.csv",
)

Onde rodar a coleta e como tratar os dados coletados

Escolha da região dos workers

Rodar os workers numa região próxima — sa-east-1 (São Paulo) na AWS, por exemplo — reduz o RTT até os portais hospedados nos EUA e deixa o throughput mais previsível ao longo da coleta.

Privacidade e LGPD

Título de vaga e faixa salarial isolados normalmente não configuram dado pessoal. O ponto de atenção aparece quando o coletor também puxa nome, e-mail ou telefone de alguém — aí a LGPD passa a valer.

Trate a base como sensível por padrão: minimize os campos coletados e documente a finalidade antes de rodar qualquer scraper em produção.


Boas práticas para manter a coleta estável

Seis ajustes simples reduzem a frequência de CAPTCHA e mantêm o scraper previsível:

  • Rotação de proxy entre requisições — distribui a carga e reduz falso positivo de limite de taxa.
  • Intervalo de 3 a 5 s entre páginas — aproxima o ritmo humano e evita picos de requisição.
  • User-Agent consistente por sessão — mantém a sessão estável.
  • Aceitar cookies de sessão — os painéis usam cookies para manter o contexto da busca.
  • Randomizar a ordem das buscas — evita padrão de acesso previsível.
  • Limite de ~200 páginas/dia por domínio — mantém o volume dentro de um uso responsável.

Solução de problemas mais comuns

Cinco sintomas cobrem quase todos os bloqueios encontrados em produção:

  • CAPTCHA em toda busca — IP sinalizado ou limite de requisições excedido; troque de IP e aumente o intervalo entre chamadas.
  • Página de resultados vazia — o portal devolveu um bloqueio de CAPTCHA no lugar da página; detecte o CAPTCHA antes de fazer o parsing.
  • "Confirme que você não é um robô" — detecção de bot acionada; use egress de rede autorizado e um User-Agent realista.
  • Login exigido para ver o salário — conteúdo restrito pela plataforma; abra uma sessão autenticada.
  • Resultado diferente do navegador — diferença de idioma, região ou cookie; alinhe o Accept-Language e a região do proxy.

Perguntas frequentes

Quais CAPTCHAs aparecem com mais frequência em portais de vagas?

Na prática, o reCAPTCHA v2 domina — Indeed, Glassdoor e Monster — com Cloudflare Turnstile no LinkedIn e no ZipRecruiter, e reCAPTCHA v3 em portais com login, como o CareerBuilder. A CaptchaAI resolve os três pela mesma API; só muda o method.

Quantas páginas de vaga dá para coletar por dia sem cair em CAPTCHA constante?

Com alguns segundos de intervalo entre requisições e rotação de proxy, 500 a 2.000 páginas por domínio ao dia costuma ser sustentável. Acima disso, o CAPTCHA passa a aparecer em quase toda busca.

Coletar título de vaga e faixa salarial em massa tem alguma restrição no Brasil?

Isolados, esses dois campos normalmente não são dado pessoal. Se o dataset carrega nome, e-mail ou telefone de alguém, a LGPD passa a valer — minimize os campos e documente a finalidade.

O CaptchaAI resolve o CAPTCHA sozinho ou preciso reescrever o scraper inteiro?

Só integra a chamada de API no ponto em que o CAPTCHA aparece na resposta — é o que _solve_and_retry faz no exemplo acima. Não precisa trocar de biblioteca nem adicionar navegador ao pipeline.


Guias relacionados

Para escalar a coleta além do que este guia cobre, veja também:


Colete dados do mercado de trabalho em grande escala — crie sua conta na CaptchaAI e resolva CAPTCHA automaticamente.

Os comentários estão desativados para este artigo.