Por que construir um rastreador de ranking Google em Python com proxies residenciais
Se você acompanha SEO, sabe que posições no Google oscilam diariamente. Uma verificação isolada não revela tendências — você precisa de snapshots diários de SERP armazenados em série temporal para identificar ganhos, perdas e volatilidade. Construir um rastreador de ranking Google em Python com proxies residenciais é a forma mais custo-eficiente de obter esse dado sem depender de APIs de terceiros que cobram por palavra-chave.
O problema é que o Google endureceu drasticamente a raspagem de SERP em 2025-2026. A remoção do parâmetro num=100 em setembro de 2025, o fingerprinting TLS JA3/JA4 e a pontuação de reputação de IP tornaram proxies datacenter quase inúteis para SERP scraping. Proxies residenciais com geo-targeting em nível de cidade são agora o padrão para rastreamento confiável.
Este guia mostra como implementar um rastreador de produção: modelo de dados, paginação SERP, contornação de fingerprint, rotação de IP com o ProxyHat, armazenamento em SQLite e endurecimento para produção.
Modelo de dados: por que snapshots diários superam verificações pontuais
Um rastreador de ranking precisa de um esquema que capture quem, o quê, onde, quando e em que posição. O modelo mínimo:
- keyword — termo pesquisado (ex.: "melhor proxy residencial")
- target_domain — domínio que você monitora (ex.: "proxyhat.com")
- country — código ISO 3166-1 alpha-2 (ex.: "US", "BR", "DE")
- device — "desktop" ou "mobile"
- position — posição orgânica (1-100) ou null se não aparece
- captured_at — timestamp UTC da captura
Com snapshots diários, você calcula métricas que uma verificação isolada nunca revelaria: volatilidade de 7 dias, tendência de 30 dias, flutuações por dispositivo e diferenças por país. Um domínio pode estar em #3 no desktop nos EUA e #8 no mobile no Brasil — sem série temporal, você não saberia.
Esquema SQLite
import sqlite3
from datetime import datetime, timezone
def init_db(db_path="rank_tracker.db"):
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS rank_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
target_domain TEXT NOT NULL,
country TEXT NOT NULL,
device TEXT NOT NULL,
position INTEGER,
captured_at TEXT NOT NULL,
raw_url TEXT,
UNIQUE(keyword, target_domain, country, device, captured_at)
)
""")
conn.execute("""
CREATE INDEX IF NOT EXISTS idx_keyword_domain
ON rank_history(keyword, target_domain, country, device, captured_at)
""")
conn.commit()
return conn
def insert_snapshot(conn, keyword, domain, country, device, position, url=None):
conn.execute("""
INSERT OR REPLACE INTO rank_history
(keyword, target_domain, country, device, position, captured_at, raw_url)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (keyword, domain, country, device, position,
datetime.now(timezone.utc).isoformat(), url))
conn.commit()
O constraint UNIQUE evita duplicatas se você executar o mesmo snapshot duas vezes no mesmo dia. Para análise, uma query simples calcula a média móvel de 7 dias por palavra-chave.
Contexto técnico: por que a raspagem de SERP ficou mais difícil
O Google aplica múltiplas camadas de defesa contra raspagem automatizada:
- Fingerprint TLS/JA3-JA4: O Google inspeciona o handshake TLS do cliente. Bibliotecas HTTP padrão (requests, urllib3) produzem fingerprints que não correspondem a navegadores reais, acionando CAPTCHAs ou blocos. Segundo a IETF TLS Working Group, o fingerprint JA3 hash combina versão TLS, cifras suportadas e extensões — dois clientes com o mesmo JA3 são tratados como idênticos.
- Pontuação de reputação de IP: IPs datacenter (ASNs de provedores de nuvem) recebem scores baixos, resultando em mais CAPTCHAs e resultados distorcidos. Proxies residenciais usam IPs de ISPs reais, com scores mais altos.
- Remoção de num=100: Em setembro de 2025, o Google desativou o parâmetro
num=100que permitia buscar 100 resultados em uma única página. Agora você precisa paginar comstart=0,10,20.... - Variação geográfica: Resultados diferem por país, cidade e dispositivo. Sem geo-targeting preciso, seus dados são ruído.
Para contornar fingerprint TLS, usamos curl_cffi com impersonate='chrome', que reproduz o handshake TLS do Chrome. Para reputação de IP e geo-targeting, usamos proxies residenciais do ProxyHat.
Paginação SERP pós-setembro de 2025: start=0,10,20
Com num=100 removido, cada página de resultados contém ~10 resultados orgânicos. Para cobrir o top 100, você precisa de 10 requisições paginadas com start=0,10,20,30...90. Cada requisição deve usar um IP residencial diferente para evitar padrões de paginação detectáveis.
from urllib.parse import urlencode, quote_plus
def build_serp_url(keyword, country="US", device="desktop", start=0):
params = {
"q": keyword,
"hl": "en",
"gl": country,
"num": 10,
"start": start,
}
base = "https://www.google.com/search"
return f"{base}?{urlencode(params)}"
def build_all_pages(keyword, country="US", max_results=100):
urls = []
for start in range(0, max_results, 10):
urls.append(build_serp_url(keyword, country, start=start))
return urls
Para dispositivos móveis, adicione &udm=7 ou use o user-agent apropriado. O parâmetro gl controla o país dos resultados.
Por que proxies residenciais com geo-targeting em nível de cidade são obrigatórios
Proxies datacenter falham para SERP scraping porque o Google reconhece seus ASNs. Proxies residenciais usam IPs de provedores de internet reais (Comcast, AT&T, Vodafone), tornando o tráfego indistinguível de um usuário legítimo.
Além disso, o Google personaliza resultados por localização. Se você rastreia rankings para o mercado de Chicago, um IP de Nova York pode retornar resultados diferentes. O ProxyHat permite geo-targeting em nível de país e cidade:
| Tipo de proxy | Reputação de IP | Geo-targeting | Custo relativo | Indicado para SERP |
|---|---|---|---|---|
| Datacenter | Baixa | País | $ | Não |
| Residencial | Alta | País + Cidade | $$ | Sim |
| Móvel | Muito alta | País | $$$ | Sim (alto volume) |
Para a maioria dos casos de rastreamento de ranking, proxies residenciais oferecem o melhor equilíbrio entre custo e confiabilidade. Verifique os locais disponíveis do ProxyHat para confirmar cobertura das cidades que você precisa.
Implementação: curl_cffi + ProxyHat para raspagem de SERP
Agora vamos combinar tudo: curl_cffi para contornar fingerprint TLS, proxies residenciais do ProxyHat para IP e geo-targeting, e parsing de posições orgânicas.
Configuração do ProxyHat
O ProxyHat usa um gateway único com flags no username para controle de geo-targeting e sessão:
# HTTP proxy (porta 8080)
# Username com geo-targeting país + cidade e sessão sticky
proxy_http = "http://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:8080"
# SOCKS5 proxy (porta 1080)
proxy_socks5 = "socks5://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:1080"
A flag -session-kw123 mantém o mesmo IP durante a sessão — importante para paginar o top 100 de uma palavra-chave sem trocar de IP a cada página (o que pareceria suspeito). Use um identificador de sessão único por palavra-chave.
Buscando SERP com curl_cffi
import re
from curl_cffi import requests as cffi_requests
from datetime import datetime, timezone
def fetch_serp_page(url, proxy_url, device="desktop"):
"""Busca uma página de SERP via curl_cffi com impersonação Chrome."""
ua = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
) if device == "desktop" else (
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) "
"Version/17.0 Mobile/15E148 Safari/604.1"
)
headers = {
"User-Agent": ua,
"Accept-Language": "en-US,en;q=0.9",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}
resp = cffi_requests.get(
url,
headers=headers,
proxies={"http": proxy_url, "https": proxy_url},
impersonate="chrome",
timeout=30,
allow_redirects=True,
)
resp.raise_for_status()
return resp.text
def detect_captcha(html):
"""Detecta CAPTCHAs e páginas de bloqueio do Google."""
captcha_signals = [
"unusual traffic",
"captcha",
"detected unusual traffic",
"/sorry/index",
"recaptcha",
]
html_lower = html.lower()
return any(sig in html_lower for sig in captcha_signals)
Parsing de resultados orgânicos
O Google não usa classes CSS estáveis para resultados orgânicos. A abordagem mais robusta combina seletores CSS com regex fallback:
from bs4 import BeautifulSoup
from urllib.parse import urlparse, unquote
def parse_organic_results(html, target_domain):
"""Extrai posições orgânicas e encontra o domínio alvo.
Retorna (position, url) ou (None, None) se não encontrado."""
soup = BeautifulSoup(html, "html.parser")
results = []
# Seletores CSS comuns para resultados orgânicos (2025-2026)
selectors = [
"div.g div.yuRUbf > a",
"div.g > div > div > a",
"div[data-sokoban-container] a[href^='/url']",
"a.jsl[href^='/url?q=']",
]
for selector in selectors:
links = soup.select(selector)
if links:
for link in links:
href = link.get("href", "")
if href.startswith("/url?q="):
# Formato /url?q=ENCODED_URL&sa=...
href = unquote(href.split("q=")[1].split("&")[0])
if href.startswith("http") and "google.com" not in urlparse(href).netloc:
results.append(href)
if results:
break
# Fallback: regex para /url?q= padrões
if not results:
pattern = r'/url\?q=(https?://[^&"]+)'
results = [unquote(m) for m in re.findall(pattern, html)]
# Deduplicar preservando ordem
seen = set()
organic = []
for url in results:
if url not in seen:
seen.add(url)
organic.append(url)
# Encontrar posição do domínio alvo
target = target_domain.lower().replace("www.", "")
for i, url in enumerate(organic, 1):
netloc = urlparse(url).netloc.lower().replace("www.", "")
if netloc == target or netloc.endswith("." + target):
return i, url
return None, None
Orquestração completa: rastrear uma palavra-chave
import time
import random
import logging
from datetime import datetime, timezone
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("rank_tracker")
def track_keyword(keyword, target_domain, country="US", city=None,
device="desktop", max_results=100, db_conn=None,
proxy_user="user", proxy_pass="pass"):
"""Rastreia a posição de um domínio para uma palavra-chave.
Pagina o top 100 usando proxies residenciais com sessão sticky."""
session_id = f"kw-{hash(keyword) % 100000}"
geo = f"country-{country}"
if city:
geo += f"-city-{city}"
proxy_url = f"http://{proxy_user}-{geo}-session-{session_id}:{proxy_pass}@gate.proxyhat.com:8080"
position_found = None
found_url = None
for start in range(0, max_results, 10):
url = build_serp_url(keyword, country=country, device=device, start=start)
logger.info(f"Buscando: {keyword} | start={start} | proxy_geo={geo}")
for attempt in range(3):
try:
html = fetch_serp_page(url, proxy_url, device=device)
if detect_captcha(html):
logger.warning(f"CAPTCHA detectado em start={start}, tentativa {attempt+1}")
# Trocar sessão para obter novo IP
new_session = f"kw-{hash(keyword) % 100000}-r{attempt}"
proxy_url = f"http://{proxy_user}-{geo}-session-{new_session}:{proxy_pass}@gate.proxyhat.com:8080"
time.sleep(5 * (attempt + 1))
continue
pos, found = parse_organic_results(html, target_domain)
if pos:
# Ajustar posição com base na página atual
absolute_pos = start + pos
position_found = absolute_pos
found_url = found
logger.info(f"Encontrado: {target_domain} na posição {absolute_pos}")
break # Sair do loop de tentativas
else:
logger.info(f"Não encontrado nesta página (start={start})")
break
except Exception as e:
logger.error(f"Erro em start={start}: {e}")
time.sleep(2 ** attempt + random.uniform(0, 1))
if position_found:
break # Domínio encontrado, não precisa paginar mais
# Delay entre páginas para parecer humano
time.sleep(random.uniform(2, 5))
if db_conn:
insert_snapshot(db_conn, keyword, target_domain, country, device,
position_found, found_url)
return position_found, found_url
Endurecimento para produção
Um rastreador de produção precisa lidar com falhas, concorrência e variações. Aqui estão as práticas essenciais:
Retries com backoff exponencial
import random
from functools import wraps
def retry_with_backoff(max_retries=3, base_delay=2.0):
"""Decorador para retries com backoff exponencial + jitter."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exc = None
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
last_exc = e
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
logger.warning(
f"Tentativa {attempt+1}/{max_retries} falhou: {e}. "
f"Aguardando {delay:.1f}s..."
)
time.sleep(delay)
raise last_exc
return wrapper
return decorator
@retry_with_backoff(max_retries=4, base_delay=3.0)
def fetch_with_retry(url, proxy_url, device="desktop"):
return fetch_serp_page(url, proxy_url, device)
Concorrência controlada
Para rastrear centenas de palavras-chave, use concorrência limitada. asyncio com semáforo evita sobrecarregar o gateway:
import asyncio
from curl_cffi.requests import AsyncSession
async def track_keywords_async(keywords, target_domain, country="US",
max_concurrent=5, **kwargs):
"""Rastreia múltiplas palavras-chave com concorrência limitada."""
sem = asyncio.Semaphore(max_concurrent)
results = {}
async def _track(kw):
async with sem:
# Em produção, use AsyncSession do curl_cffi
loop = asyncio.get_event_loop()
pos, url = await loop.run_in_executor(
None, track_keyword, kw, target_domain, country
)
results[kw] = (pos, url)
# Delay entre keywords para evitar burst
await asyncio.sleep(random.uniform(3, 8))
tasks = [_track(kw) for kw in keywords]
await asyncio.gather(*tasks)
return results
Com 5 conexões concorrentes e 3-8s de delay entre palavras-chave, você rastreia ~500 palavras-chave por hora por país. Para volumes maiores, aumente a concorrência gradualmente e monitore taxas de sucesso.
Detecção e tratamento de CAPTCHA
Quando o Google detecta tráfego suspeito, retorna uma página de CAPTCHA em vez de resultados. Seu rastreador deve:
- Detectar a página de CAPTCHA (função
detect_captchaacima). - Trocar de IP — mude a flag
-session-para obter um novo IP residencial. - Backoff — aguarde 30-60s antes de tentar novamente com novo IP.
- Registrar — marque o snapshot como "bloqueado" para análise posterior.
Se a taxa de CAPTCHA exceder 5% das requisições, reduza a concorrência ou aumente os delays. Consulte a documentação oficial do Google sobre crawlers para entender como o Google classifica tráfego automatizado.
Pools de proxy por país
Para rastrear múltiplos países, use pools separados com geo-targeting específico:
COUNTRY_CONFIG = {
"US": {"city": "chicago", "gl": "us", "hl": "en"},
"BR": {"city": "sao-paulo", "gl": "br", "hl": "pt-BR"},
"DE": {"city": "berlin", "gl": "de", "hl": "de"},
"UK": {"city": "london", "gl": "uk", "hl": "en"},
}
def get_proxy_for_country(country, keyword, user, pwd):
cfg = COUNTRY_CONFIG.get(country, {"gl": country.lower(), "hl": "en"})
city = cfg.get("city", "")
geo = f"country-{country}"
if city:
geo += f"-city-{city}"
session = f"kw-{hash(keyword) % 100000}"
return f"http://{user}-{geo}-session-{session}:{pwd}@gate.proxyhat.com:8080"
Suavização de volatilidade de ranking
Posições diárias são ruidosas. Para análise, use média móvel de 7 dias:
def get_rank_trend(conn, keyword, domain, country, device, days=7):
"""Retorna a média móvel de posição dos últimos N dias."""
cursor = conn.execute("""
SELECT captured_at, position FROM rank_history
WHERE keyword=? AND target_domain=? AND country=? AND device=?
AND position IS NOT NULL
ORDER BY captured_at DESC
LIMIT ?
""", (keyword, domain, country, device, days))
rows = cursor.fetchall()
if not rows:
return None
avg = sum(r[1] for r in rows) / len(rows)
return round(avg, 1)
Erros comuns e casos extremos
- Ignorar resultados pagos: Anúncios do Google Ads aparecem no topo da SERP. Seu parser deve pular
div[data-text-ad] e elementos com data-pla (Shopping ads). Contar anúncios como posições orgânicas inflaciona falsamente seu ranking.
- Não filtrar SERP features: Featured snippets, People Also Ask, map packs e carrosséis de imagens não são resultados orgânicos padrão. Seu domínio pode aparecer em #0 (featured snippet) mas não no bloco orgânico — decida como contar isso.
- Usar o mesmo IP para todas as páginas: Paginar 10 páginas com o mesmo IP cria um padrão de raspagem óbvio. Use sessão sticky por palavra-chave, mas troque entre palavras-chave.
- Ignorar redirecionamentos: O Google pode redirecionar para
consent.google.com em alguns países (especialmente UE). Seu cliente deve seguir redirecionamentos e lidar com páginas de consentimento de cookies.
- Não registrar falhas: Sempre registre snapshots "não encontrado" (position=null) e "bloqueado" separadamente. Um salto de #5 para null pode ser um bloqueio, não uma queda real de ranking.
Configuração específica do ProxyHat
div[data-text-ad] e elementos com data-pla (Shopping ads). Contar anúncios como posições orgânicas inflaciona falsamente seu ranking.consent.google.com em alguns países (especialmente UE). Seu cliente deve seguir redirecionamentos e lidar com páginas de consentimento de cookies.O ProxyHat oferece um gateway unificado em gate.proxyhat.com com controle total via username. Para rastreamento de ranking, recomendamos:
- Proxies residenciais para SERP scraping — IPs de ISP reais com alta reputação.
- Geo-targeting por cidade para resultados precisos por mercado local.
- Sessão sticky por palavra-chave para consistência durante paginação.
- Rotação entre palavras-chave para distribuir tráfego entre IPs.
Consulte os documentos oficiais do ProxyHat para detalhes de autenticação e recursos avançados. Para verificar planos e preços, visite a página de preços do ProxyHat. Para casos de uso relacionados, veja raspagem web e rastreamento de SERP.
Dica de produção: Comece com 10-20 palavras-chave, monitore a taxa de sucesso por 48h e ajuste delays/concorrência. Uma taxa de sucesso de 95%+ é o objetivo. Se cair abaixo de 90%, reduza a concorrência ou aumente o delay entre requisições.
Ética e limites
Rastrear rankings é uma prática comum, mas tem limites:
- Rastreie seu próprio site e concorrentes públicos — não rastreie marcas de terceiros sem necessidade.
- Respeite rate limits — mantenha concorrência baixa (5-10 conexões) e delays de 3-8s entre requisições.
- Considere uma fonte oficial de SERP para baixo volume — a Google Custom Search API oferece 100 queries/dia gratuitas, suficiente para monitorar 10-20 palavras-chave sem proxies.
- Respeite robots.txt — embora o Google permita indexação, a raspagem de SERP pode violar os Termos de Serviço do Google. Use por sua conta e risco.
- GDPR/CCPA — dados de SERP não contêm PII, mas armazene timestamps e metadados de acordo com a legislação local.
Principais conclusões
- Snapshots diários em série temporal são superiores a verificações isoladas — revelam tendências e volatilidade.
- Paginação com start=0,10,20 é necessária após a remoção de num=100 em setembro de 2025.
- curl_cffi com impersonate='chrome' contorna fingerprint TLS/JA3 que bloqueia requests/urllib3.
- Proxies residenciais com geo-targeting por cidade são obrigatórios — datacenter proxies recebem CAPTCHAs quase imediatamente.
- Sessão sticky por palavra-chave mantém consistência durante paginação do top 100.
- SQLite com constraint UNIQUE simplifica armazenamento e evita duplicatas.
- Retries com backoff exponencial e detecção de CAPTCHA são essenciais para produção.
- Para baixo volume, considere a Google Custom Search API — 100 queries/dia grátis sem necessidade de proxies.
Perguntas frequentes
O que é um rastreador de ranking Google em Python com proxies residenciais?
É um sistema que coleta posições de um domínio no Google para palavras-chave específicas, usando Python para automação e proxies residenciais para contornar bloqueios de IP. O rastreador busca páginas de SERP, analisa resultados orgânicos, identifica a posição do domínio alvo e armazena o histórico em série temporal para análise de tendências.
Por que rastreadores de ranking precisam de proxies residenciais?
O Google aplica fingerprint TLS/JA3-JA4 e pontuação de reputação de IP. Proxies datacenter têm ASNs reconhecíveis que recebem scores baixos, resultando em CAPTCHAs e resultados distorcidos. Proxies residenciais usam IPs de ISPs reais, com reputação alta, tornando o tráfego indistinguível de usuários legítimos. Sem proxies residenciais, a taxa de bloqueio pode exceder 50% após poucas requisições.
Qual tipo de proxy funciona melhor para rastreamento de ranking Google?
Proxies residenciais com geo-targeting em nível de cidade oferecem o melhor equilíbrio entre custo e confiabilidade para SERP scraping. Proxies móveis têm reputação ainda mais alta, mas custam mais. Proxies datacenter não são recomendados para SERP. Use sessão sticky por palavra-chave para manter consistência durante paginação do top 100.
Como evitar bloqueios ao implementar um rastreador de ranking Google?
Use curl_cffi com impersonate='chrome' para contornar fingerprint TLS, proxies residenciais com geo-targeting por cidade, sessão sticky por palavra-chave, concorrência limitada (5-10 conexões), delays de 3-8s entre requisições, detecção de CAPTCHA com troca de IP, e retries com backoff exponencial. Monitore a taxa de sucesso e ajuste se cair abaixo de 90%.
É possível rastrear rankings do Google sem proxies?
Para baixo volume (até 100 queries/dia), a Google Custom Search API oferece uma alternativa oficial e gratuita sem necessidade de proxies. Para volumes maiores ou rastreamento de SERP orgânica completa (top 100), proxies residenciais são praticamente obrigatórios devido às defesas anti-raspagem do Google.





