Saber cuándo un competidor gana o pierde clasificación es información útil para los equipos de SEO, estrategas de contenido y especialistas en marketing de productos. La mayoría de las herramientas de seguimiento de clasificación procesan por lotes una vez a la semana y ocultan los datos de la competencia en paneles. Este tutorial crea un rastreador liviano que consulta sus palabras clave objetivo a través de la API de Scavio, registra qué competidores aparecen y en qué posición, se diferencia de la instantánea anterior e imprime alertas cuando un competidor ingresa al top 10, abandona o se mueve más de tres posiciones. Todo el estado cabe en un único archivo JSON y el script se ejecuta en menos de un minuto para 20 palabras clave.
Requisitos previos
- Python 3.8 o superior instalado
- solicita biblioteca instalada
- Una clave API de Scavio de scavio.dev
- Una lista de palabras clave objetivo y dominios de la competencia
Guia paso a paso
Paso 1: Definir palabras clave y dominios de la competencia
Enumere las palabras clave que está rastreando y los dominios de la competencia que desea monitorear. El script marcará cualquier cambio de posición específicamente para estos dominios.
COMPETITORS = ["competitor-a.com", "competitor-b.com", "competitor-c.com"]
KEYWORDS = [
"best crm software 2026",
"crm for small business",
"free crm tool",
"crm vs spreadsheet"
]Paso 2: Obtenga datos SERP y extraiga posiciones de la competencia
Para cada palabra clave, consulta el SERP y registra la posición de cada dominio de la competencia que aparece entre los 10 primeros.
import os
import requests
API_KEY = os.environ.get("SCAVIO_API_KEY", "your_scavio_api_key")
ENDPOINT = "https://api.scavio.dev/api/v1/search"
def get_competitor_positions(query: str, competitors: list[str]) -> dict:
r = requests.post(
ENDPOINT,
headers={"x-api-key": API_KEY},
json={"query": query, "country_code": "us"}
)
r.raise_for_status()
positions = {}
for item in r.json().get("organic_results", []):
link = item.get("link", "")
for comp in competitors:
if comp in link:
positions[comp] = item["position"]
return positionsPaso 3: Comparar con la instantánea anterior y generar alertas
Compare las posiciones actuales con la última instantánea guardada. Alerta sobre nuevas entradas, salidas y cambios significativos de posición.
def diff_positions(keyword: str, current: dict, previous: dict) -> list[str]:
alerts = []
all_domains = set(list(current.keys()) + list(previous.keys()))
for domain in all_domains:
old_pos = previous.get(domain)
new_pos = current.get(domain)
if new_pos and not old_pos:
alerts.append(f"[NEW] {domain} entered top 10 at #{new_pos} for '{keyword}'")
elif old_pos and not new_pos:
alerts.append(f"[GONE] {domain} dropped out of top 10 for '{keyword}' (was #{old_pos})")
elif old_pos and new_pos and abs(old_pos - new_pos) >= 3:
direction = "up" if new_pos < old_pos else "down"
alerts.append(f"[MOVE] {domain} moved {direction} #{old_pos} -> #{new_pos} for '{keyword}'")
return alertsPaso 4: Ejecute el rastreador y mantenga el estado
Ejecute el ciclo de seguimiento completo, imprima alertas y guarde el estado actual para la siguiente ejecución.
import json
from datetime import date
STATE_FILE = "competitor_serp_state.json"
def load_state() -> dict:
try:
with open(STATE_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
return {}
def run_tracker() -> None:
state = load_state()
new_state = {}
all_alerts = []
for kw in KEYWORDS:
positions = get_competitor_positions(kw, COMPETITORS)
previous = state.get(kw, {})
alerts = diff_positions(kw, positions, previous)
all_alerts.extend(alerts)
new_state[kw] = positions
for alert in all_alerts:
print(alert)
if not all_alerts:
print("No significant changes detected.")
with open(STATE_FILE, "w") as f:
json.dump(new_state, f, indent=2)
print(f"State saved. Checked {len(KEYWORDS)} keywords.")
run_tracker()Ejemplo en Python
import os
import json
import requests
from datetime import date
API_KEY = os.environ.get("SCAVIO_API_KEY", "your_scavio_api_key")
ENDPOINT = "https://api.scavio.dev/api/v1/search"
STATE_FILE = "competitor_serp_state.json"
COMPETITORS = ["competitor-a.com", "competitor-b.com"]
KEYWORDS = ["best crm software 2026", "crm for small business", "free crm tool"]
def get_positions(query: str) -> dict:
r = requests.post(
ENDPOINT,
headers={"x-api-key": API_KEY},
json={"query": query, "country_code": "us"}
)
r.raise_for_status()
positions = {}
for item in r.json().get("organic_results", []):
for comp in COMPETITORS:
if comp in item.get("link", ""):
positions[comp] = item["position"]
return positions
def diff(keyword: str, current: dict, previous: dict) -> list[str]:
alerts = []
for domain in set(list(current) + list(previous)):
old, new = previous.get(domain), current.get(domain)
if new and not old:
alerts.append(f"[NEW] {domain} entered #{new} for '{keyword}'")
elif old and not new:
alerts.append(f"[GONE] {domain} left top 10 for '{keyword}'")
elif old and new and abs(old - new) >= 3:
alerts.append(f"[MOVE] {domain} #{old}->{new} for '{keyword}'")
return alerts
def track() -> None:
try:
with open(STATE_FILE) as f:
state = json.load(f)
except FileNotFoundError:
state = {}
new_state, alerts = {}, []
for kw in KEYWORDS:
pos = get_positions(kw)
alerts.extend(diff(kw, pos, state.get(kw, {})))
new_state[kw] = pos
for a in alerts:
print(a)
with open(STATE_FILE, "w") as f:
json.dump(new_state, f, indent=2)
print(f"Tracked {len(KEYWORDS)} keywords, {len(alerts)} alerts")
if __name__ == "__main__":
track()Ejemplo en JavaScript
const fs = require("fs");
const API_KEY = process.env.SCAVIO_API_KEY || "your_scavio_api_key";
const ENDPOINT = "https://api.scavio.dev/api/v1/search";
const STATE = "competitor_serp_state.json";
const COMPETITORS = ["competitor-a.com", "competitor-b.com"];
const KEYWORDS = ["best crm software 2026", "crm for small business"];
async function getPositions(query) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ query, country_code: "us" })
});
const data = await res.json();
const positions = {};
for (const item of data.organic_results || []) {
for (const comp of COMPETITORS) {
if ((item.link || "").includes(comp)) positions[comp] = item.position;
}
}
return positions;
}
async function track() {
let state = {};
try { state = JSON.parse(fs.readFileSync(STATE, "utf-8")); } catch {}
const newState = {};
for (const kw of KEYWORDS) {
const pos = await getPositions(kw);
const prev = state[kw] || {};
for (const d of new Set([...Object.keys(pos), ...Object.keys(prev)])) {
if (pos[d] && !prev[d]) console.log(`[NEW] ${d} entered #${pos[d]} for '${kw}'`);
else if (prev[d] && !pos[d]) console.log(`[GONE] ${d} left top 10 for '${kw}'`);
else if (prev[d] && pos[d] && Math.abs(prev[d] - pos[d]) >= 3)
console.log(`[MOVE] ${d} #${prev[d]}->#${pos[d]} for '${kw}'`);
}
newState[kw] = pos;
}
fs.writeFileSync(STATE, JSON.stringify(newState, null, 2));
}
track().catch(console.error);Salida esperada
[NEW] competitor-b.com entered top 10 at #7 for 'best crm software 2026'
[MOVE] competitor-a.com moved up #8 -> #4 for 'crm for small business'
[GONE] competitor-c.com dropped out of top 10 for 'free crm tool'
State saved. Checked 4 keywords.