Construir um Rank Tracker do Google em Python com Proxies Residenciais

Guia prático para engenheiros SEO e desenvolvedores Python: modelo de dados, paginação de SERPs, proxies residenciais com geo-targeting de cidade, curl_cffi e ProxyHat SDK, endurecimento de produção e ética.

Build a Google Rank Tracker in Python with Residential Proxies
Neste artigo

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 proxyReputação de IPGeo-targetingTaxa de sucesso (estimada)Custo relativo
DatacenterBaixa (marcado como bot)País apenas30-50%$
Residencial rotativoAlta (ISP real)País + cidade90-98%$$$
MóvelMuito altaPaís + cidade95-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,...,90 para cobrir o top 100 após a remoção de num=100.
  • Proxies: residenciais com -country-US-city-chicago-session-kw123 no username, via gate.proxyhat.com:8080.
  • TLS: curl_cffi com impersonate="chrome" é obrigatório — requests puro é 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.

Perguntas frequentes

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.

Pronto para começar?

Proxies residenciais, ISP e móveis em mais de 148 países. Crie uma conta grátis.

Criar conta grátis
← Voltar ao Blog