Se você precisa construir um rank tracker do Google em Python com proxies residenciais, este guia entrega um pipeline completo: modelo de dados, paginação de SERPs pós-setembro de 2025, fingerprinting TLS, geo-targeting de cidade, curl_cffi com ProxyHat SDK, endurecimento de produção e limites éticos. O foco é código executável — você pode copiar, adaptar e colocar em produção.
Por que construir um rank tracker do Google em Python com proxies residenciais ainda importa
Rastrear posições de palavras-chave manualmente não escala. Uma agência com 200 clientes e 50 palavras-chave cada precisa de 10.000 verificações por dia. Em volume assim, três problemas aparecem rápido: bloqueios de IP, variação geográfica de resultados e volatilidade de ranking. Um rank tracker automatizado resolve os três ao combinar snapshots diários, proxies residenciais com geo-targeting e persistência de histórico.
O Google removeu o parâmetro num=100 em setembro de 2025, conforme documentado por referências da comunidade de SEO e artigos sobre SERP. Antes, uma única requisição retornava 100 resultados orgânicos; agora você precisa paginar com start=0,10,20... até cobrir o top 100. Isso multiplica o número de requisições por 10 e torna a gestão de proxies ainda mais crítica.
Proxies residenciais importam porque o Google aplica fingerprinting TLS (JA3/JA4) e scoring de reputação de IP. Datacenter proxies usam blocos de IP que o Google marca como tráfego automatizado, resultando em CAPTCHAs e resultados distorcidos. IPs residenciais pertencem a ISPs reais, têm reputação orgânica e suportam geo-targeting em nível de cidade — essencial para rastrear rankings locais com precisão.
Contexto técnico: por que o problema existe
Três mudanças no ecossistema do Google tornaram o scraping de SERPs mais difícil entre 2024 e 2026:
- Remoção de
num=100— agora o máximo por página é 10 resultados orgânicos, exigindo paginação. - Fingerprinting TLS ativo — o Google inspeciona o ClientHello TLS e bloqueia clientes HTTP Python padrão (como
requests) que não imitam um navegador real. - Scoring de reputação de IP — IPs de datacenter são classificados como bots com alta probabilidade, mesmo com headers corretos.
A consequência prática: um rank tracker que usa requests + proxies de datacenter terá uma taxa de sucesso abaixo de 40% em volume moderado (500+ requisições/dia). Com curl_cffi impersonando Chrome e proxies residenciais com sticky sessions, a taxa de sucesso sobe para 95%+.
Modelo de dados: por que snapshots diários superam verificações pontuais
Uma verificação pontual diz onde seu domínio está agora. Um snapshot diário diz para onde está indo. Rastreamento de ranking sem histórico é como dirigir olhando apenas pelo retrovisor — você não vê tendências, não detecta quedas repentinas e não pode suavizar volatilidade.
O modelo de dados mínimo para um rank tracker:
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
class Device(str, Enum):
DESKTOP = "desktop"
MOBILE = "mobile"
class SearchType(str, Enum):
ORGANIC = "organic"
LOCAL_PACK = "local_pack"
FEATURED_SNIPPET = "featured_snippet"
@dataclass
class Keyword:
id: int
term: str
target_domain: str # ex: "example.com"
country: str # ISO 3166-1 alpha-2, ex: "US"
city: str | None # ex: "chicago"
device: Device
language: str # ex: "en"
@dataclass
class RankSnapshot:
id: int
keyword_id: int
position: int | None # None se não encontrado no top 100
page: int # 1-10
result_url: str | None
captured_at: datetime
search_type: SearchType
captcha_encountered: bool
proxy_ip: str # para auditoria
Armazenar proxy_ip e captcha_encountered permite diagnosticar problemas de qualidade dos proxies e correlacionar quedas de ranking com bloqueios. Sem esses campos, você não consegue distinguir uma queda real de um CAPTCHA mal detectado.
Buscando SERPs após a remoção de num=100
Com a remoção de num=100, você precisa paginar usando o parâmetro start. Para cobrir o top 100, faça requisições com start=0, 10, 20, ..., 90 — dez requisições por palavra-chave. Cada requisição deve usar a mesma sessão sticky para manter consistência de personalização.
import asyncio
from urllib.parse import urlencode, quote_plus
def build_google_url(keyword: str, country: str, city: str | None,
device: str, start: int, language: str = "en") -> str:
"""Constrói URL de busca do Google com parâmetros de geo e dispositivo."""
params = {
"q": keyword,
"hl": language,
"gl": country,
"num": 10,
"start": start,
}
if city:
# Google não tem parâmetro oficial de cidade; uule é um workaround
# conhecido, mas instável. Preferimos geo-targeting via proxy.
pass
base = "https://www.google.com/search"
return f"{base}?{urlencode(params)}"
# Paginação do top 100
def generate_pages() -> list[int]:
return list(range(0, 100, 10)) # [0, 10, 20, ..., 90]
Parsing de resultados orgânicos
Após buscar o HTML, você precisa extrair os resultados orgânicos. O Google não usa classes CSS estáveis — elas mudam sem aviso. Uma estratégia robusta combina seletores CSS com fallback em regex:
import re
from bs4 import BeautifulSoup
from dataclasses import dataclass
@dataclass
class OrganicResult:
position: int
url: str
title: str
def parse_organic_results(html: str, start: int) -> list[OrganicResult]:
"""Extrai resultados orgânicos, pulando anúncios e SERP features."""
soup = BeautifulSoup(html, "html.parser")
results: list[OrganicResult] = []
# Seletor principal — atualize quando o Google mudar a marcação
divs = soup.select("div.g")
if not divs:
# Fallback: buscar links dentro de divs com role="main"
divs = soup.select("div[role='main'] div")
position_offset = start # start=0 -> posição 1; start=10 -> posição 11
for idx, div in enumerate(divs):
link = div.select_one("a[href^='/url?q=']") or div.select_one("a[href^='http']")
if not link:
continue
href = link.get("href", "")
# Decodificar redirect do Google (/url?q=...)
if href.startswith("/url?q="):
href = href.split("/url?q=")[1].split("&")[0]
# Pular anúncios (geralmente em divs com classe contendo 'ad')
parent_classes = " ".join(div.get("class", []))
if "ads" in parent_classes.lower() or "commercial" in parent_classes.lower():
continue
title_el = div.select_one("h3")
title = title_el.get_text(strip=True) if title_el else ""
# Pular SERP features (people-also-ask, knowledge panel, etc.)
if not href.startswith("http"):
continue
position = position_offset + idx + 1
results.append(OrganicResult(position=position, url=href, title=title))
return results
Por que proxies residenciais com geo-targeting de cidade são obrigatórios
O Google personaliza resultados por localização. Se você rastreia "pizza delivery" de um IP em São Paulo, verá resultados de São Paulo — inútil se seu cliente opera em Chicago. Proxies residenciais com geo-targeting resolvem isso.
| Tipo de proxy | Reputação de IP | Geo-targeting | Taxa de sucesso (estimada) | Custo relativo |
|---|---|---|---|---|
| Datacenter | Baixa (marcado como bot) | País apenas | 30-50% | $ |
| Residencial rotativo | Alta (ISP real) | País + cidade | 90-98% | $$$ |
| Móvel | Muito alta | País + cidade | 95-99% | $$$$ |
Para rank tracking, residenciais rotativos com sticky sessions oferecem o melhor equilíbrio. Móveis são superiores mas caros demais para volume diário. Datacenter é falso econômico — o custo dos bloqueios supera a economia.
Com o ProxyHat, o geo-targeting e a sessão sticky vão no username:
# HTTP com geo-targeting de cidade + sessão sticky por palavra-chave
# Formato: user-country-US-city-chicago-session-keyword123:pass
# curl
curl -x http://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:8080 \
"https://www.google.com/search?q=pizza+delivery&hl=en&gl=US&num=10&start=0"
# SOCKS5 (use quando HTTP for bloqueado)
# curl -x socks5://user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:1080 ...
A flag -session-kw123 mantém o mesmo IP durante toda a paginação de uma palavra-chave. Sem sticky sessions, cada página pode vir de um IP diferente, e o Google retorna resultados personalizados inconsistentes.
Exemplo prático: curl_cffi + ProxyHat SDK
O curl_cffi é uma biblioteca Python que usa libcurl com impersonação de TLS. Ela replica o ClientHello do Chrome, Firefox ou Safari, evitando o fingerprinting JA3/JA4 do Google. Combinada com proxies residenciais do ProxyHat, é a stack mais robusta para scraping de SERPs em 2026.
import asyncio
import csv
import sqlite3
from datetime import datetime
from curl_cffi.requests import AsyncSession
class GoogleRankTracker:
def __init__(self, proxy_user: str, proxy_pass: str):
self.gateway = "gate.proxyhat.com"
self.port = 8080
self.proxy_user = proxy_user
self.proxy_pass = proxy_pass
def _proxy_url(self, country: str, city: str | None, session_id: str) -> str:
"""Monta a URL do proxy ProxyHat com geo + sessão sticky."""
user = f"{self.proxy_user}-country-{country}"
if city:
user += f"-city-{city}"
user += f"-session-{session_id}"
return f"http://{user}:{self.proxy_pass}@{self.gateway}:{self.port}"
async def fetch_serp(self, keyword: str, country: str,
city: str | None, device: str,
language: str = "en") -> str:
"""Busca todas as 10 páginas do top 100 e retorna HTML concatenado."""
session_id = keyword.replace(" ", "_")[:20]
proxy = self._proxy_url(country, city, session_id)
headers = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/131.0.0.0 Safari/537.36"
),
"Accept-Language": f"{language},en;q=0.9",
}
async with AsyncSession(impersonate="chrome", proxies={"http": proxy, "https": proxy}) as s:
all_html = []
for start in range(0, 100, 10):
url = "https://www.google.com/search"
params = {"q": keyword, "hl": language, "gl": country,
"num": 10, "start": start}
try:
resp = await s.get(url, params=params, headers=headers,
timeout=15, allow_redirects=True)
if resp.status_code == 429:
await asyncio.sleep(5)
continue
if "captcha" in resp.text.lower()[:500]:
raise RuntimeError(f"CAPTCHA detectado em start={start}")
all_html.append(resp.text)
await asyncio.sleep(2) # rate limiting entre páginas
except Exception as e:
print(f"Erro em start={start}: {e}")
# Continua para a próxima página; registro parcial
return "\n".join(all_html)
@staticmethod
def find_position(html: str, target_domain: str, start: int = 0) -> int | None:
"""Encontra a posição do domínio alvo no HTML de uma página."""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
divs = soup.select("div.g")
for idx, div in enumerate(divs):
link = div.select_one("a[href]")
if not link:
continue
href = link.get("href", "")
if href.startswith("/url?q="):
href = href.split("/url?q=")[1].split("&")[0]
if target_domain in href:
return start + idx + 1
return None
def save_to_sqlite(self, snapshot, db_path: str = "rank_history.db"):
"""Persiste snapshot no SQLite."""
conn = sqlite3.connect(db_path)
conn.execute("""
CREATE TABLE IF NOT EXISTS rank_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT NOT NULL,
target_domain TEXT NOT NULL,
country TEXT NOT NULL,
city TEXT,
device TEXT NOT NULL,
position INTEGER,
result_url TEXT,
captured_at TEXT NOT NULL,
captcha_encountered INTEGER DEFAULT 0,
proxy_ip TEXT
)
""")
conn.execute(
"INSERT INTO rank_snapshots VALUES (NULL,?,?,?,?,?,?,?,?,?,?)",
(snapshot["keyword"], snapshot["target_domain"],
snapshot["country"], snapshot.get("city"),
snapshot["device"], snapshot["position"],
snapshot.get("result_url"), snapshot["captured_at"],
int(snapshot.get("captcha_encountered", False)),
snapshot.get("proxy_ip", ""))
)
conn.commit()
conn.close()
def export_csv(self, db_path: str = "rank_history.db",
csv_path: str = "rank_history.csv"):
"""Exporta histórico para CSV."""
conn = sqlite3.connect(db_path)
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["keyword", "target_domain", "country", "city",
"device", "position", "captured_at"])
for row in conn.execute("SELECT keyword, target_domain, country, "
"city, device, position, captured_at "
"FROM rank_snapshots ORDER BY captured_at"):
writer.writerow(row)
conn.close()
Uso do tracker:
async def main():
tracker = GoogleRankTracker(proxy_user="meu_user", proxy_pass="minha_senha")
keyword = "pizza delivery chicago"
target_domain = "example.com"
country = "US"
city = "chicago"
device = "desktop"
html = await tracker.fetch_serp(keyword, country, city, device)
position = tracker.find_position(html, target_domain)
snapshot = {
"keyword": keyword,
"target_domain": target_domain,
"country": country,
"city": city,
"device": device,
"position": position,
"result_url": None,
"captured_at": datetime.utcnow().isoformat(),
"captcha_encountered": False,
"proxy_ip": "n/a",
}
tracker.save_to_sqlite(snapshot)
print(f"Posição de {target_domain} para '{keyword}': {position or 'não encontrado'}")
asyncio.run(main())
Endurecimento de produção
Retries com backoff exponencial
import random
import asyncio
from functools import wraps
def retry_with_backoff(max_retries: int = 5, base_delay: float = 1.0,
max_delay: float = 60.0):
"""Decorator para retry com backoff exponencial + jitter."""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except Exception as e:
if attempt == max_retries - 1:
raise
delay = min(base_delay * (2 ** attempt) + random.uniform(0, 1),
max_delay)
print(f"Tentativa {attempt+1} falhou: {e}. "
f"Aguardando {delay:.1f}s...")
await asyncio.sleep(delay)
return wrapper
return decorator
@retry_with_backoff(max_retries=5)
async def fetch_with_retry(session, url, **kwargs):
return await session.get(url, **kwargs)
Detecção de CAPTCHA
O Google serve CAPTCHAs de várias formas. Detectar cedo evita desperdiçar requisições:
def detect_captcha(html: str) -> bool:
"""Detecta sinais de CAPTCHA ou bloqueio no HTML."""
signals = [
"captcha",
"unusual traffic",
"automated requests",
"sorry/image",
"recaptcha",
"g-recaptcha",
"detected unusual traffic",
]
lower = html.lower()[:2000] # só o início é suficiente
return any(sig in lower for sig in signals)
# No loop de fetch:
if detect_captcha(resp.text):
# Trocar de sessão/IP e aguardar mais tempo
session_id = f"{keyword[:10]}_{int(time.time())}"
await asyncio.sleep(30)
continue
Limites de concorrência por país
Executar 200 palavras-chave em paralelo contra o Google de um único país é pedir para ser bloqueado. Use semáforos para limitar concorrência:
from collections import defaultdict
# Máximo 5 requisições concorrentes por país
country_semaphores: dict[str, asyncio.Semaphore] = defaultdict(
lambda: asyncio.Semaphore(5)
)
async def fetch_with_country_limit(country: str, fetch_func, *args, **kwargs):
sem = country_semaphores[country]
async with sem:
return await fetch_func(*args, **kwargs)
Suavização de volatilidade de ranking
Posições oscilam dia a dia. Uma média móvel de 7 dias reduz ruído e destaca tendências reais:
def moving_average(positions: list[int | None], window: int = 7) -> list[float | None]:
"""Média móvel tratando None (não encontrado) como posição 101."""
padded = [p if p is not None else 101 for p in positions]
result = []
for i in range(len(padded)):
start = max(0, i - window + 1)
window_vals = padded[start:i+1]
result.append(sum(window_vals) / len(window_vals))
return result
Ética e limites
Rastrear rankings é legal quando você monitora seus próprios domínios ou dados públicos sem violar Termos de Serviço. Mas há limites:
- Respeite
robots.txt— o Google permite indexação de sua SERP, mas o scraping em volume pode violar os Termos de Serviço. - Rate limiting — mantenha abaixo de 1 requisição por segundo por IP. Com 100 IPs residenciais, isso dá 100 req/s, suficiente para a maioria dos casos.
- Volume baixo — se você precisa de menos de 100 requisições/dia, considere a Google Custom Search API oficial. É paga mas estável e em conformidade.
- GDPR/CCPA — não colete dados pessoais de SERPs (nomes, e-mails, telefones em snippets).
Para volumes maiores, a ProxyHat oferece planos de proxies residenciais com geo-targeting em nível de cidade. Consulte as localizações disponíveis e veja casos de uso de SERP tracking e web scraping para detalhes de implementação. A documentação completa está em docs.proxyhat.com.
Principais conclusões
Construir um rank tracker do Google em Python com proxies residenciais exige cinco peças trabalhando juntas: modelo de dados com histórico, paginação de SERPs, curl_cffi com impersonação de TLS, proxies residenciais com geo-targeting de cidade e sessões sticky, e endurecimento de produção com retries, detecção de CAPTCHA e limites de concorrência.
- Modelo de dados: armazene keyword, domínio alvo, país, cidade, dispositivo, posição, URL do resultado, timestamp, flag de CAPTCHA e IP do proxy. Sem histórico, você não detecta tendências.
- Paginação: use
start=0,10,20,...,90para cobrir o top 100 após a remoção denum=100. - Proxies: residenciais com
-country-US-city-chicago-session-kw123no username, viagate.proxyhat.com:8080. - TLS:
curl_cfficomimpersonate="chrome"é obrigatório —requestspuro é detectado por fingerprinting JA3/JA4. - Produção: retries com backoff exponencial + jitter, detecção de CAPTCHA, semáforos por país (máx 5 concorrentes) e média móvel de 7 dias para suavizar volatilidade.
- Ética: rastreie seus próprios domínios, respeite rate limits e prefira a API oficial do Google para volumes baixos.
FAQ
O que é construir um rank tracker do Google em Python com proxies residenciais?
É o processo de criar um sistema automatizado em Python que busca posições de palavras-chave no Google usando proxies residenciais para evitar bloqueios. O sistema inclui modelo de dados para histórico, paginação de SERPs, parsing de resultados orgânicos, persistência em SQLite/CSV e endurecimento de produção com retries e detecção de CAPTCHA. Proxies residenciais são necessários porque o Google bloqueia IPs de datacenter via fingerprinting TLS e scoring de reputação.
Por que proxies residenciais importam para rank tracking do Google?
Proxies residenciais importam porque o Google aplica fingerprinting TLS (JA3/JA4) e scoring de reputação de IP. IPs de datacenter são marcados como tráfego automatizado, resultando em CAPTCHAs e resultados distorcidos. IPs residenciais pertencem a ISPs reais, têm reputação orgânica e suportam geo-targeting em nível de cidade, essencial para rastrear rankings locais com precisão. A taxa de sucesso sobe de 30-50% (datacenter) para 90-98% (residencial).
Qual tipo de proxy funciona melhor para rank tracking do Google?
Proxies residenciais rotativos com sessões sticky oferecem o melhor equilíbrio entre custo e confiabilidade para rank tracking. Proxies móveis têm taxa de sucesso ainda maior (95-99%) mas são caros demais para volume diário. Proxies de datacenter têm taxa de sucesso baixa (30-50%) porque o Google os marca como bots. Com o ProxyHat, use o formato user-country-US-city-chicago-session-kw123:pass@gate.proxyhat.com:8080 para geo-targeting de cidade e sessão sticky por palavra-chave.
Como evitar bloqueios ao construir um rank tracker do Google em Python?
Para evitar bloqueios: use curl_cffi com impersonate="chrome" para replicar o fingerprint TLS do navegador; use proxies residenciais com geo-targeting de cidade e sessões sticky por palavra-chave; mantenha concorrência abaixo de 5 requisições por país; implemente retries com backoff exponencial e jitter; detecte CAPTCHAs cedo e troque de IP ao detectar; respeite rate limits de 1 req/s por IP; e aguarde 2-3 segundos entre páginas de paginação.
Como paginar SERPs do Google após a remoção de num=100?
Após a remoção de num=100 em setembro de 2025, use o parâmetro start para paginar: start=0 retorna posições 1-10, start=10 retorna 11-20, e assim por diante até start=90 para cobrir o top 100. Cada requisição deve usar a mesma sessão sticky do proxy para manter consistência de personalização. Aguarde 2-3 segundos entre páginas para evitar detecção de comportamento automatizado.





