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 |
| 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-Languagee 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:
- Rotação de proxy para coletas de maior volume
- Como a qualidade do proxy afeta a taxa de resolução
- Sessões fixas vs. rotativas na coleta de dados
Colete dados do mercado de trabalho em grande escala — crie sua conta na CaptchaAI e resolva CAPTCHA automaticamente.