A coleta de benchmark de remuneração costuma empacar na terceira consulta, e o culpado raramente é o parser: é o Cloudflare Turnstile entre a busca e a tabela de faixas salariais. A resposta curta: envie a sitekey à API da CaptchaAI, consulte o resultado, reenvie a requisição com o token cf-turnstile-response e siga a coleta. Este guia mostra o padrão em Python e JavaScript, com os limites de volume e o cuidado de LGPD que dados de remuneração exigem.
Onde o CAPTCHA aparece em uma pesquisa salarial
Times de People Analytics costumam descobrir isso tarde demais: o script roda bem com cinco cargos e quebra em 400 combinações de cargo e cidade.
| Tipo de fonte | CAPTCHA | O que dispara |
|---|---|---|
| Sites de comparação de salários | Cloudflare Turnstile | Buscas repetidas na mesma sessão |
| Filtros salariais de quadros de vagas | reCAPTCHA v2 | Várias consultas de faixa seguidas |
| Estatísticas trabalhistas públicas | CAPTCHA de imagem | Pedidos de download de bases |
| Páginas salariais corporativas | Cloudflare Turnstile em staging | Volume alto de visualizações |
| Plataformas de pesquisa de RH | reCAPTCHA v3 | Envio de formulários |
Repare no padrão: o gatilho é quase sempre a repetição, não a suspeita de bot — uma consulta isolada passa, a décima encontra o widget.
Regime de coleta e frequência de CAPTCHA nas consultas salariais
| Abordagem | Volume por dia | Frequência de CAPTCHA | Mais adequada para |
|---|---|---|---|
| Sequencial com intervalos | 100 a 500 consultas | Baixa | Pesquisas pontuais de um time |
| Rotação de egress autorizado | 500 a 2.000 consultas | Moderada | Análise por região |
| Paralelo multissessão | 2.000 a 10.000 consultas | Alta | Bases anuais completas |
Os planos da CaptchaAI são cobrados por thread simultânea, com resoluções ilimitadas no mês. O BASIC (US$ 15/mês, 5 threads) cobre uma pesquisa sequencial de algumas centenas de consultas; o ADVANCE (US$ 90/mês, 50 threads) é o degrau quando a coleta vira multissessão. Sem tarifa por resolução, um pico de CAPTCHAs não estoura o orçamento — só ocupa mais threads.
Coletor de dados salariais em Python
O script consulta o portal, detecta o Turnstile pela presença de cf-turnstile no HTML (ou pelo 403) e reenvia a requisição com o token resolvido. O time.sleep(2) entre consultas não é enfeite: é o que segura a frequência de desafios.
import requests
import time
import re
from dataclasses import dataclass
@dataclass
class SalaryRecord:
title: str
location: str
min_salary: float
max_salary: float
median_salary: float
sample_size: int
source: str
class SalaryCollector:
def __init__(self, api_key):
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def collect_salary_data(self, portal_url, job_title, location):
"""Search for salary data, solving CAPTCHAs as needed."""
response = self.session.get(portal_url, params={
"title": job_title,
"location": location
})
if self._is_turnstile_challenge(response):
response = self._solve_turnstile_and_retry(response, portal_url)
return self._parse_salary_data(response.text, portal_url)
def collect_bulk(self, portal_url, job_titles, locations):
"""Collect salary data for multiple job title + location combos."""
results = []
for title in job_titles:
for location in locations:
try:
data = self.collect_salary_data(
portal_url, title, location
)
results.extend(data)
# Respectful delay between requests
time.sleep(2)
except Exception as e:
print(f"Failed for {title} in {location}: {e}")
return results
def _is_turnstile_challenge(self, response):
return (
response.status_code == 403 or
"cf-turnstile" in response.text or
"challenges.cloudflare.com" in response.text
)
def _solve_turnstile_and_retry(self, response, url):
match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
if not match:
raise ValueError("Turnstile sitekey not found")
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "turnstile",
"sitekey": match.group(1),
"pageurl": url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return self.session.post(url, data={
"cf-turnstile-response": data["request"]
})
raise TimeoutError("Turnstile solve timed out")
def _parse_salary_data(self, html, source):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
records = []
def text_or_empty(node):
return node.text.strip() if node and node.text else ""
for row in soup.select(".salary-row, .compensation-entry, tr[data-salary]"):
try:
records.append(SalaryRecord(
title=text_or_empty(row.select_one(".job-title, .title")),
location=text_or_empty(row.select_one(".location")),
min_salary=self._parse_amount(
text_or_empty(row.select_one(".min-salary, .low"))
),
max_salary=self._parse_amount(
text_or_empty(row.select_one(".max-salary, .high"))
),
median_salary=self._parse_amount(
text_or_empty(row.select_one(".median, .mid"))
),
sample_size=int(
text_or_empty(row.select_one(".count, .sample")).replace(",", "") or 0
),
source=source
))
except (AttributeError, ValueError):
continue
return records
def _parse_amount(self, text):
if not text:
return 0.0
cleaned = re.sub(r'[^\d.]', '', text)
return float(cleaned) if cleaned else 0.0
# Usage
collector = SalaryCollector("YOUR_API_KEY")
data = collector.collect_bulk(
"https://salary.example.com/search",
job_titles=["Software Engineer", "Data Analyst", "Product Manager"],
locations=["San Francisco", "New York", "Austin"]
)
for record in data:
print(f"{record.title} in {record.location}: "
f"${record.min_salary:,.0f}–${record.max_salary:,.0f} "
f"(median: ${record.median_salary:,.0f})")
Dois detalhes contam. O laço for _ in range(60) dá três minutos de janela, mas o Turnstile costuma ser resolvido em menos de 10 s: na prática ele sai na segunda volta. E _parse_salary_data engole AttributeError e ValueError linha a linha — portais mudam classes CSS, e perder três registros é melhor que perder a coleta inteira.
Agregação de múltiplas fontes em JavaScript
Uma faixa vinda de uma fonte só é opinião; o valor aparece ao cruzar portais. O agregador abaixo isola cada fonte em um try/catch: portal fora do ar vira campo error no resultado, não exceção que derruba o lote.
class SalaryAggregator {
constructor(apiKey) {
this.apiKey = apiKey;
this.sources = [];
}
addSource(name, searchUrl) {
this.sources.push({ name, searchUrl });
}
async collectForRole(jobTitle, location) {
const results = [];
for (const source of this.sources) {
try {
const data = await this.querySource(source, jobTitle, location);
results.push({ source: source.name, ...data });
} catch (error) {
results.push({ source: source.name, error: error.message });
}
}
return this.aggregateResults(results, jobTitle, location);
}
async querySource(source, jobTitle, location) {
const url = `${source.searchUrl}?title=${encodeURIComponent(jobTitle)}&location=${encodeURIComponent(location)}`;
const response = await fetch(url);
const html = await response.text();
if (html.includes('cf-turnstile') || response.status === 403) {
return this.solveAndRetry(source.searchUrl, html, jobTitle, location);
}
return this.parseSalaryData(html);
}
async solveAndRetry(baseUrl, html, jobTitle, location) {
const match = html.match(/data-sitekey="(0x[^"]+)"/);
if (!match) throw new Error('Turnstile sitekey not found');
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: new URLSearchParams({
key: this.apiKey,
method: 'turnstile',
sitekey: match[1],
pageurl: baseUrl,
json: '1'
})
});
const { request: taskId } = await submitResp.json();
for (let i = 0; i < 60; i++) {
await new Promise(r => setTimeout(r, 3000));
const result = await fetch(
`https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
);
const data = await result.json();
if (data.status === 1) {
const response = await fetch(baseUrl, {
method: 'POST',
body: new URLSearchParams({
'cf-turnstile-response': data.request,
title: jobTitle,
location: location
})
});
return this.parseSalaryData(await response.text());
}
}
throw new Error('Turnstile solve timed out');
}
aggregateResults(results, jobTitle, location) {
const valid = results.filter(r => !r.error && r.median);
if (valid.length === 0) return null;
const medians = valid.map(r => r.median);
return {
jobTitle,
location,
avgMedian: medians.reduce((a, b) => a + b, 0) / medians.length,
sources: valid.length,
range: { min: Math.min(...medians), max: Math.max(...medians) }
};
}
}
// Usage
const aggregator = new SalaryAggregator('YOUR_API_KEY');
aggregator.addSource('SalaryDB', 'https://salarydb.example.com/search');
aggregator.addSource('PayScale', 'https://payscale.example.com/lookup');
const result = await aggregator.collectForRole('Software Engineer', 'San Francisco');
console.log(`Median salary: $${result.avgMedian.toLocaleString()} (${result.sources} sources)`);
Um cenário brasileiro: benchmark de cargos de tecnologia
Uma área de People Analytics em São Paulo monta a faixa de 12 cargos de engenharia para a revisão anual, cruzando três portais públicos: 12 cargos × 4 regiões × 3 fontes = 144 consultas. Sequencialmente, com 2 s de intervalo e desafios em parte das requisições, o lote fecha em cerca de 15 minutos — e as 5 threads do BASIC bastam, porque nunca há mais de uma resolução em andamento.
Dois cuidados:
- Rode o worker perto da fonte. Com portais brasileiros, um worker em
sa-east-1corta dezenas de milissegundos de RTT por requisição — irrelevante em 144 consultas, relevante quando o recorte vira 10 mil. - Trate a LGPD desde o desenho. Faixas agregadas por cargo e região não identificam ninguém, e é assim que a coleta deve permanecer: medianas, nunca perfis associáveis a uma pessoa. Documente a base legal e a retenção antes da primeira execução e valide com o jurídico da sua empresa — este texto não é orientação legal.
Colete apenas de fontes públicas cujos termos permitam e mantenha os testes de integração em staging.example.com ou no seu próprio ambiente.
Quando a coleta trava: diagnóstico rápido
| Sintoma | Causa provável | Correção |
|---|---|---|
| Turnstile em toda busca, mesmo lenta | Sessão perdida entre requisições | Reutilize o mesmo objeto de sessão e persista os cookies |
| Resposta diz "Login obrigatório" | Portal exige conta autenticada | Autentique antes de buscar e reaproveite a sessão |
| Resultado vazio após resolver o CAPTCHA | Campos ocultos do formulário faltando | Reenvie todos os input[type=hidden] junto com o token |
| Faixas diferentes a cada execução | Parâmetros de consulta inconsistentes | Fixe moeda, período e nível de senioridade na query |
| Token aceito no cURL, recusado no navegador | Token enviado ao endpoint errado | Compare os dois fluxos requisição a requisição antes de mexer no parser |
Se o token é resolvido mas o portal insiste no 403, o problema está no reenvio, não na resolução: Turnstile devolvendo 403 mesmo com o token correto.
Perguntas frequentes
Quanto tempo a CaptchaAI leva para resolver um Turnstile de portal salarial?
O Cloudflare Turnstile costuma ser resolvido em menos de 10 s. Numa coleta com 2 s de intervalo, o CAPTCHA raramente é o gargalo — o limite de requisições do portal chega antes.
Preciso de um navegador headless para coletar dados salariais?
Na maioria dos casos, não. Se a página já entrega os dados na primeira requisição, requests ou fetch bastam e a resolução acontece por API. O headless só se justifica quando as faixas são renderizadas por JavaScript após interação.
Qual plano cobre uma pesquisa salarial trimestral?
Depende da concorrência, não do total de resoluções. Lotes sequenciais de algumas centenas de consultas cabem no BASIC (US$ 15/mês, 5 threads); coletas multissessão com milhares de combinações pedem ADVANCE (US$ 90/mês, 50 threads) ou acima. Todos os planos têm resoluções ilimitadas por thread.
E se o portal salarial usar um tipo de desafio fora da lista suportada?
Confira os tipos cobertos antes de investir no coletor: reCAPTCHA v2 e v3, Cloudflare Turnstile e Challenge, GeeTest v3 e CAPTCHAs de imagem/OCR. O hCaptcha e o FunCaptcha não estão nessa lista, e o GeeTest v4 aparece apenas como "em breve". Nesses casos, vale escolher outra fonte pública para a mesma faixa de cargo.
Como armazeno o resultado sem retrabalho na próxima rodada?
Grave cada registro com a fonte, a data e os parâmetros da consulta, como faz o campo source do SalaryRecord. Sem procedência, comparar duas rodadas vira adivinhação.
Artigos relacionados
- Coleta de dados para pesquisa de mercado
- GeeTest ou Cloudflare Turnstile: qual desafio você vai encontrar
- Turnstile devolvendo 403 mesmo com o token correto
Próximos passos
Tire o CAPTCHA do caminho da sua pesquisa de remuneração: gere sua chave de API na CaptchaAI e rode a primeira coleta hoje.