Como monitorar rankings de keywords com Python

Um rank tracker mínimo em Python: consulta, extrai a posição de organic_results, guarda no SQLite, e a matemática de créditos por trás da profundidade.

Um rank tracker não precisa ser um produto. Para um punhado de keywords e um domínio, é um loop, um arquivo de banco de dados, e uma forma de perceber o dia em que algo se moveu. Aqui está tudo em cerca de sessenta linhas.

O formato

Quatro passos, rodados em um schedule: manda cada keyword pelo /serp com país e location fixos, encontra seu domínio em organic_results, escreve a posição no SQLite, e compara com a linha de ontem.

import sqlite3
import requests
from datetime import date
from urllib.parse import urlparse

API_KEY = "faf_live_..."
DB_PATH = "rankings.db"
DOMAIN = "fastaf.co"
COUNTRY = "us"

KEYWORDS = [
    "web scraping api",
    "serp api pricing",
    "google search api alternative",
]


def init_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS rankings (
            checked_on TEXT,
            keyword TEXT,
            domain TEXT,
            position INTEGER,
            PRIMARY KEY (checked_on, keyword, domain)
        )
    """)
    return conn

Buscando uma posição

organic_results já vem com um campo rank em cada item, então encontrar o seu é uma questão de comparar o hostname de link com o domínio, exato ou como subdomínio (um simples domain in link também casa com https://example.org/review/fastaf.co). Profundidade é um loop sobre page, já que o Google não retorna mais 100 resultados em uma requisição:

def on_domain(link, domain):
    host = urlparse(link).hostname or ""
    return host == domain or host.endswith("." + domain)


def find_position(keyword, domain, country=COUNTRY, location=None, max_pages=1):
    for page in range(max_pages):
        params = {"query": keyword, "country": country, "page": page}
        if location:
            params["location"] = location

        resp = requests.get(
            "https://api.fastaf.co/v1/serp",
            params=params,
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=60,
        )
        resp.raise_for_status()

        for result in resp.json().get("organic_results", []):
            if on_domain(result.get("link", ""), domain):
                return result["rank"]

    return None  # não encontrado dentro de max_pages

location restringe a busca a um lugar específico (uma cidade, por exemplo) em vez de só um país, o que importa para qualquer coisa com intenção local. page é o controle de profundidade: cada página é uma requisição faturável separada, dez resultados por vez.

Monitorando uma lista e salvando a rodada do dia

def track(conn, keywords, domain, max_pages=1):
    today = date.today().isoformat()
    for keyword in keywords:
        position = find_position(keyword, domain, max_pages=max_pages)
        conn.execute(
            "INSERT OR REPLACE INTO rankings VALUES (?, ?, ?, ?)",
            (today, keyword, domain, position),
        )
        print(f"{keyword}: {position}")
    conn.commit()


if __name__ == "__main__":
    conn = init_db()
    track(conn, KEYWORDS, DOMAIN)

Rode isso uma vez por dia (cron, uma função agendada, o que você já usa) e rankings.db acumula uma linha por keyword por dia.

Calculando o delta diário

O objetivo de guardar histórico é o delta, não o snapshot. Puxe as últimas duas linhas de cada keyword e compare:

def deltas(conn, domain):
    rows = conn.execute("""
        SELECT keyword, checked_on, position
        FROM rankings
        WHERE domain = ?
        ORDER BY keyword, checked_on
    """, (domain,)).fetchall()

    by_keyword = {}
    for keyword, checked_on, position in rows:
        by_keyword.setdefault(keyword, []).append((checked_on, position))

    for keyword, history in by_keyword.items():
        if len(history) < 2:
            continue
        (_, prev), (_, curr) = history[-2], history[-1]
        if prev is None or curr is None:
            continue
        change = prev - curr  # positivo significa que subiu
        if change != 0:
            print(f"{keyword}: {prev} -> {curr} ({change:+d})")

A matemática de créditos por trás da “profundidade”

Essa é a parte que silenciosamente destrói um orçamento de créditos se não for dimensionada com antecedência. Uma requisição /serp custa 5 créditos independente da profundidade, mas “top 100” são dez requisições separadas, não uma, já que o Google não respeita mais uma única requisição retornando 100 resultados.

Configuração Requisições/dia Créditos/dia Créditos (30 dias) Pacote que cobre
500 keywords, diário, profundidade 10 (só page 0) 500 2.500 75.000 Growth, $29
500 keywords, diário, profundidade 100 (10 páginas) 5.000 25.000 750.000 Pro, $299

Profundidade 10 checa se você aparece na primeira página, o que basta para a maioria dos trackings. Profundidade 100 é o que você precisa se também quiser saber onde está entre as páginas 2 e 10, e custa dez vezes mais para a mesma lista de keywords. Defina max_pages deliberadamente, não por hábito: um tracker que sobe de profundidade 10 para profundidade 100 sem ninguém perceber é uma fatura em créditos 10x maior para a mesma lista de keywords.

Checar duas vezes por dia em vez de uma dobra todo número dessa tabela; checar semanalmente em vez de diariamente divide por sete. Escolha uma frequência que o movimento de ranking que importa para você realmente exige, não o intervalo mais curto que seu scheduler suporta.

Resumo

Um rank tracker é uma chamada /serp, uma comparação de domínio contra organic_results, e uma tabela SQLite, nada mais exótico que isso. O número que decide sua fatura é profundidade vezes frequência vezes quantidade de keywords, não a ferramenta. Calcule isso antes de agendar o job: os preços têm o custo em créditos por pacote, e a documentação cobre o conjunto completo de parâmetros do /serp, incluindo location e device.