Cómo monitorizar el ranking de palabras clave con Python

Un rank tracker mínimo en Python: consulta, extrae tu posición de organic_results, guárdala en SQLite, y las cuentas en créditos detrás de la profundidad.

Un rank tracker no tiene por qué ser un producto. Para un puñado de palabras clave y un dominio, es un bucle, un archivo de base de datos, y una forma de detectar el día en que algo se movió. Aquí está todo en unas sesenta líneas.

La forma que tiene

Cuatro pasos, ejecutados con una programación: envía cada palabra clave por /serp con un país y una ubicación fijos, busca tu dominio en organic_results, escribe la posición en SQLite, y compárala con la fila de ayer.

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

Consultar una posición

organic_results viene con un campo rank ya en cada entrada, así que encontrar la tuya es cuestión de comparar el hostname de link con el dominio, exacto o como subdominio (un simple domain in link también coincide con https://example.org/review/fastaf.co). La profundidad es un bucle sobre page, ya que Google ya no devuelve 100 resultados en una sola solicitud:

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  # no se encontró dentro de max_pages

location acota la búsqueda a un lugar concreto (una ciudad, por ejemplo) en vez de solo un país, algo que importa para cualquier cosa con intención local. page es el control de profundidad: cada página es una solicitud facturable aparte, diez resultados cada vez.

Monitorizar una lista y guardar la ejecución del día

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)

Ejecuta esto una vez al día (cron, una función programada, lo que ya uses) y rankings.db va acumulando una fila por palabra clave y día.

Calcular el delta diario

El sentido de guardar historial es el delta, no la foto suelta. Extrae las dos últimas filas de cada palabra clave y compáralas:

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 subió
        if change != 0:
            print(f"{keyword}: {prev} -> {curr} ({change:+d})")

Las cuentas en créditos detrás de la “profundidad”

Esta es la parte que arruina en silencio un presupuesto de créditos si no se dimensiona de antemano. Una solicitud /serp cuesta 5 créditos sin importar la profundidad, pero el “top 100” son diez solicitudes separadas, no una, ya que Google ya no honra una sola solicitud que devuelva 100 resultados.

Configuración Solicitudes/día Créditos/día Créditos (30 días) Pack que lo cubre
500 keywords, diario, profundidad 10 (solo página 0) 500 2.500 75.000 Growth, 29 $
500 keywords, diario, profundidad 100 (10 páginas) 5.000 25.000 750.000 Pro, 299 $

Profundidad 10 comprueba si apareces en la primera página, lo cual basta para la mayoría del seguimiento. Profundidad 100 es lo que necesitas si además quieres saber dónde estás entre las páginas 2 y 10, y cuesta diez veces más por la misma lista de palabras clave. Fija max_pages de forma deliberada, no por costumbre: un tracker subido en silencio de profundidad 10 a profundidad 100 es una factura de créditos 10 veces mayor por la misma lista de palabras clave.

Comprobar dos veces al día en vez de una duplica cada número de esa tabla; comprobar semanalmente en vez de a diario los divide entre siete. Elige una frecuencia que de verdad necesite el movimiento de ranking que te importa, no el intervalo más corto que soporte tu programador de tareas.

La versión corta

Un rank tracker es una llamada a /serp, una comparación de dominio contra organic_results, y una tabla SQLite, nada más exótico que eso. El número que decide tu factura es profundidad por frecuencia por número de palabras clave, no la herramienta. Calcúlalo antes de programar el job: precios tiene el coste en créditos por pack, y la documentación cubre el conjunto completo de parámetros de /serp, incluyendo location y device.