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.