Construir um Rastreador de Ranking Google em Python com Proxies Residenciais

Guia prático para desenvolvedores: construa um rastreador de posições Google em Python com proxies residenciais, paginação SERP, contornação de fingerprint TLS/JA3 e armazenamento em SQLite.

Build a Google Rank Tracker in Python with Residential Proxies

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:

  1. 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.
  2. 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.
  3. Remoção de num=100: Em setembro de 2025, o Google desativou o parâmetro num=100 que permitia buscar 100 resultados em uma única página. Agora você precisa paginar com start=0,10,20....
  4. 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 proxyReputação de IPGeo-targetingCusto relativoIndicado para SERP
DatacenterBaixaPaís$Não
ResidencialAltaPaís + Cidade$$Sim
MóvelMuito altaPaí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:

  1. Detectar a página de CAPTCHA (função detect_captcha acima).
  2. Trocar de IP — mude a flag -session- para obter um novo IP residencial.
  3. Backoff — aguarde 30-60s antes de tentar novamente com novo IP.
  4. 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

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.

Pronto para começar?

Acesse mais de 50M de IPs residenciais em mais de 148 países com filtragem por IA.

Ver preçosProxies residenciais
← Voltar ao Blog