Blog

Reporting Automatizado con Google Search Console API 2026

Reporting automatizado con GSC API

El reporting manual es una de las tareas más tediosas del SEO: exportar CSV desde Search Console, formatear datos en Excel, crear gráficos, enviar reporte por email. Repetir. Cada. Semana.

Google Search Console API resuelve este problema. Pero tiene una barrera de entrada: OAuth 2.0, autenticación, rate limits, tokens que expiran. Parece complicado.

No lo es. Vamos a construir un framework completo de reporting automatizado con Python que:

  • Se conecta a GSC API con OAuth 2.0 (guía paso a paso incluida)
  • Genera 4 tipos de reportes automáticamente
  • Exporta en múltiples formatos (JSON, CSV, HTML, email)
  • Guarda históricos y calcula comparativas MoM/YoY
  • Envía alertas cuando hay cambios significativos

Por qué automatizar reporting con GSC API

Problemas del reporting manual:

  • Perderás tiempo: 1-2 horas por reporte, cada semana
  • Error humano: Copy-paste mistakes, fórmulas que se rompen
  • No escalable: 1 sitio está bien, 10 clientes es un nightmare
  • Reactividad vs proactividad: Ves el pasado, no detectas cambios en tiempo real

Ventajas del reporting automatizado:

  • Ahorra 20+ horas/mes: Scripts que ejecutan en segundos
  • 100% preciso: Sin error humano, datos consistentes
  • Escalable: Añade sitios, clientes, keywords sin costo extra
  • Proactivo: Alertas automáticas cuando algo cambia

Qué vas a construir

Vamos a crear un framework con 4 reportes modulares:

  • Reporte Performance Diario — Métricas agregadas del sitio completo
  • Rankings Tracker — Seguimiento de keywords específicas con históricos
  • Landing Pages Analysis — Top páginas + oportunidades de optimización
  • Comparativa MoM/YoY — Comparación automática con deltas calculados

Cada reporte es independiente pero comparte el mismo framework de autenticación y utilidades.

Prerrequisitos

  • Cuenta de Google con acceso a Search Console
  • Python 3.10+ instalado
  • Propiedad verificada en GSC (mínimo 48h de datos)
  • Ganas de automatizar tu trabajo SEO

OAuth 2.0: Guía Completa Paso a Paso

Antes de acceder a GSC API, necesitas autenticarte. Google usa OAuth 2.0, que parece intimidante pero sigue un proceso claro.

Paso 1: Crear proyecto en Google Cloud Console

  • Navega a console.cloud.google.com
  • Click en "Seleccionar un proyecto" → "NUEVO PROYECTO"
  • Nombre: gsc-seo-reporting (o el que prefieras)
  • Click "CREAR"

Paso 2: Habilitar Search Console API

  • En el menú: "API y servicios" → "Biblioteca"
  • Busca: "Google Search Console API"
  • Click en la API → "HABILITAR"

Paso 3: Configurar OAuth 2.0 consent screen

  • Nombre de la aplicación: GSC SEO Reporting
  • Email de soporte técnico: tu email

• Click "GUARDAR Y CONTINUAR" (puedes omitir los demás campos)

Paso 4: Crear credenciales OAuth

  • En "Pantalla de consentimiento" → "Credenciales"
  • Tipo: "ID de cliente OAuth 2.0"
  • Application type: "Aplicación de ordenador"
  • Nombre: GSC Reporting Client

Paso 5: Descargar credentials JSON

  • Click "CREAR" en el cliente OAuth
  • Se descargará un archivo client_secret_XXX.apps.googleusercontent.com.json
  • Importante: Renómbralo a credentials.json y guárdalo en tu proyecto
  • ¡Nunca commits este archivo! Añádelo a .gitignore

Paso 6: Primer token de acceso

La primera vez necesitas autorizar manualmente. El script de Python hará esto por ti:

  • Ejecutar el script (ver sección de autenticación)
  • Se abrirá una ventana de navegador pidiendo permiso
  • Click "Autorizar"
  • Se generará un archivo token.json con tu access token

Nota: Los tokens expiran. El framework incluye autorenovación automática.

Troubleshooting OAuth común

Framework Base: Estructura Reutilizable

Antes de los reportes, establezcamos el framework que todos compartirán.

Estructura de proyecto

text
gsc-reporting/
├── credentials.json        # OAuth credentials (NO commitear)
├── token.json              # Access token (se genera automáticamente)
├── .env                    # Variables de entorno
├── requirements.txt        # Dependencias Python
├── config.py              # Configuración centralizada
├── auth.py                 # OAuth 2.0 handler
├── utils/
│   ├── exporter.py        # Export JSON, CSV, HTML
│   ├── alerts.py           # Alertas automáticas
│   └── database.py         # SQLite handler
├── reports/
│   ├── performance.py      # Reporte 1
│   ├── rankings.py          # Reporte 2
│   ├── landing_pages.py    # Reporte 3
│   └── comparative.py       # Reporte 4
└── main.py                 # Entry point

requirements.txt

text
google-api-python-client==2.100.0
google-auth-httplib2==0.1.1
google-auth-oauthlib==1.0.0
pandas==2.1.0
jinja2==3.1.2
python-dotenv==1.0.0

config.py - Configuración centralizada

python
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    # GSC Property URL
    PROPERTY_URL = os.getenv('PROPERTY_URL', 'https://www.example.com')
    
    # Reportes configuration
    START_DATE = os.getenv('START_DATE', '2023-01-01')
    END_DATE = os.getenv('END_DATE', '2023-12-31')
    
    # Alertas thresholds
    ALERT_CLICKS_CHANGE = float(os.getenv('ALERT_CLICKS_CHANGE', '0.2'))  # 20%
    ALERT_POSITION_CHANGE = int(os.getenv('ALERT_POSITION_CHANGE', '3'))
    
    # Database
    DB_PATH = os.getenv('DB_PATH', 'gsc_data.db')
    
    # Email alerts (opcional)
    SMTP_SERVER = os.getenv('SMTP_SERVER', 'smtp.gmail.com')
    SMTP_PORT = int(os.getenv('SMTP_PORT', '587'))
    EMAIL_USER = os.getenv('EMAIL_USER')
    EMAIL_PASSWORD = os.getenv('EMAIL_PASSWORD')
    EMAIL_TO = os.getenv('EMAIL_TO', '').split(',')

config = Config()

auth.py - OAuth 2.0 handler

python
import os
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from googleapiclient.discovery import build

SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']

def authenticate():
    """
    Autentica con GSC API usando OAuth 2.0
    Genera token.json si no existe
    Retorna cliente autenticado
    """
    creds = None
    
    # Token existe?
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    
    # Si no hay token o está expirado, renovar
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', scopes=SCOPES
            )
            flow.run_local_server(port=0)
            creds = flow.credentials
            
            # Guardar token para futuros usos
n            with open('token.json', 'w') as token:
                token.write(creds.to_json())
    
    # Retornar cliente de GSC API
    service = build('searchconsole', 'v1', credentials=creds)
    return service

Reporte 1: Performance Diario

Propósito: Métricas agregadas del sitio completo (clicks, impressions, CTR, position) en formato daily summary.

Script completo

python
# reports/performance.py
from datetime import datetime, timedelta
import pandas as pd
from auth import authenticate
from config import config
from utils.exporter import export_json, export_csv, export_html

def get_performance_report(days=7):
    """
    Obtiene reporte de performance de los últimos N días
    
    Args:
        days: Número de días a consultar
    
    Returns:
        DataFrame con datos de performance
    """
    service = authenticate()
    
    # Calcular rango de fechas
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    
    print(f"Consultando GSC API: {start_date} a {end_date}")
    
    # Consultar GSC API
    request = {
        'startDate': start_date,
        'endDate': end_date,
        'dimensions': ['date'],  # Agrupar por fecha
        'type': 'web'
    }
    
    response = service.searchanalytics().query().get(body=request).execute()
    
    # Parsear respuesta
    rows = response.get('rows', [])
    
    if not rows:
        print("No data found para el período")
        return None
    
    # Crear DataFrame
    data = []
    for row in rows:
        data.append({
            'date': row['keys'][0],
            'clicks': row['clicks'],
            'impressions': row['impressions'],
            'ctr': round(row['ctr'] * 100, 2),
            'position': round(row['position'], 2)
        })
    
    df = pd.DataFrame(data)
    df['date'] = pd.to_datetime(df['date'])
    df = df.sort_values('date')
    
    # Métricas agregadas
    summary = {
        'period': f"{start_date} to {end_date}",
        'total_clicks': int(df['clicks'].sum()),
        'total_impressions': int(df['impressions'].sum()),
        'avg_ctr': round(df['clicks'].sum() / df['impressions'].sum() * 100, 2),
        'avg_position': round(df['position'].mean(), 2),
        'days_with_data': len(df)
    }
    
    print(f"\n📊 Resumen:")
    print(f"Clicks: {summary['total_clicks']:,}")
    print(f"Impressions: {summary['total_impressions']:,}")
    print(f"CTR: {summary['avg_ctr']}%")
    print(f"Avg Position: {summary['avg_position']}")
    
    return df, summary

if __name__ == '__main__':
    df, summary = get_performance_report(days=30)
    
    if df is not None:
        # Exportar en múltiples formatos
        export_json(df, 'performance_report.json', summary)
        export_csv(df, 'performance_report.csv')
        export_html(df, 'performance_report.html', 'Performance Report', summary)
        
        print("\n✅ Reportes generados:")
        print("   - performance_report.json")
        print("   - performance_report.csv")
        print("   - performance_report.html")

utils/exporter.py - Export multi-formato

python
import pandas as pd
import json
from jinja2 import Template
from datetime import datetime

def export_json(df, filename, summary=None):
    """Exporta DataFrame a JSON"""
    output = {
        'generated_at': datetime.now().isoformat(),
        'data': df.to_dict('records'),
        'summary': summary
    }
    
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(output, f, indent=2, ensure_ascii=False)

def export_csv(df, filename):
    """Exporta DataFrame a CSV"""
    df.to_csv(filename, index=False, encoding='utf-8-sig')

def export_html(df, filename, title, summary=None):
    """Exporta DataFrame a HTML con template"""
    template = Template('''
        <!DOCTYPE html>
        <html>
        
        <body>
            <h1>{{ title }}</h1>
            
            {% if summary %}
            <div class="summary">
                <h2>Resumen</h2>
                <p><strong>Periodo:</strong> {{ summary.period }}</p>
                <p><strong>Clicks:</strong> {{ "{:,.0f}".format(summary.total_clicks) }}</p>
                <p><strong>Impressions:</strong> {{ "{:,.0f}".format(summary.total_impressions) }}</p>
                <p><strong>CTR:</strong> {{ summary.avg_ctr }}%</p>
                <p><strong>Avg Position:</strong> {{ summary.avg_position }}</p>
            </div>
            {% endif %}
            
            <table>
                <thead>
                    <tr>
                        {% for col in columns %}
                        <th>{{ col.upper() }}</th>
                        {% endfor %}
                    </tr>
                </thead>
                <tbody>
                    {% for index, row in data.iterrows() %}
                    <tr>
                        {% for col in columns %}
                        <td>{{ row[col] }}</td>
                        {% endfor %}
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
            
            <p style="color: #666; font-size: 12px;">Generated: {{ generated_at }}</p>
        </body>
        </html>
    ''')
    
    html = template.render(
        title=title,
        summary=summary,
        columns=df.columns.tolist(),
        data=df,
        generated_at=datetime.now().strftime('%Y-%m-%d %H:%M')
    )
    
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(html)

Cron job para ejecución diaria

text
# Ejecutar todos los días a las 9am
0 9 * * * cd /ruta/a/gsc-reporting && python -m reports.performance >> logs/cron.log 2>&1

# Ejecutar semanalmente los lunes a las 8am
0 8 * * 1 cd /ruta/a/gsc-reporting && python -m reports.performance >> logs/cron.log 2>&1

Reporte 2: Rankings Tracker

Propósito: Seguimiento de keywords específicas con guardado de históricos en SQLite.

Script completo

python
# reports/rankings.py
from datetime import datetime, timedelta
import sqlite3
import pandas as pd
from auth import authenticate
from config import config
from utils.database import init_db, save_ranking_snapshot

def get_keyword_rankings(keywords, days=7):
    """
    Obtiene rankings actuales para keywords específicas
    
    Args:
        keywords: Lista de keywords a trackear
        days: Días de datos a consultar
    
    Returns:
        DataFrame con rankings por keyword
    """
    service = authenticate()
    
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    
    print(f"Tracking {len(keywords)} keywords...")
    
    # Consultar GSC API para cada keyword
    all_data = []
    for keyword in keywords:
        request = {
            'startDate': start_date,
            'endDate': end_date,
            'query': keyword,
            'type': 'web'
        }
        
        try:
            response = service.searchanalytics().query().get(body=request).execute()
            rows = response.get('rows', [])
            
            if rows:
                # Agregar datos del último día disponible
                row = rows[-1]
                all_data.append({
                    'keyword': keyword,
                    'clicks': row['clicks'],
                    'impressions': row['impressions'],
                    'ctr': round(row['ctr'] * 100, 2),
                    'position': round(row['position'], 2),
                    'date': row['keys'][0]  # Fecha del dato
                })
        except Exception as e:
            print(f"Error consultando '{keyword}': {e}")
    
    if not all_data:
        print("No data encontrado")
        return None
    
    df = pd.DataFrame(all_data)
    df = df.sort_values('impressions', ascending=False)
    
    return df

def compare_with_previous(current_df, keyword):
    """
    Compara ranking actual con snapshot anterior
    
    Args:
        current_df: DataFrame con rankings actuales
        keyword: Keyword específica a comparar
    
    Returns:
        Dict con cambio de posición
    """
    conn = sqlite3.connect(config.DB_PATH)
    
    # Obtener último registro de esta keyword
    query = '''
        SELECT position, date FROM rankings_history 
        WHERE keyword = ? 
        ORDER BY date DESC 
        LIMIT 1
    '''
    
    previous = pd.read_sql_query(query, conn, params=(keyword,))
    conn.close()
    
    if previous.empty:
        return {'keyword': keyword, 'change': 'NEW', 'previous_pos': None, 'current_pos': None}
    
    previous_pos = previous.iloc[0]['position']
    current_pos = current_df[current_df['keyword'] == keyword].iloc[0]['position']
    
    position_change = previous_pos - current_pos
    
    return {
        'keyword': keyword,
        'change': position_change,
        'previous_pos': previous_pos,
        'current_pos': current_pos,
        'change_type': 'UP' if position_change > 0 else 'DOWN' if position_change < 0 else 'SAME'
    }

if __name__ == '__main__':
    # Inicializar database
    init_db()
    
    # Keywords a trackear
    KEYWORDS = [
        'neumaticos online',
        'comprar neumaticos',
        'neumaticos baratos',
        'cambiar neumaticos',
        'neumaticos coche'
    ]
    
    # Obtener rankings actuales
    df = get_keyword_rankings(KEYWORDS, days=30)
    
    if df is not None:
        print("\n📊 Rankings actuales:")
        print(df[['keyword', 'position', 'clicks', 'impressions']].to_string(index=False))
        
        # Guardar snapshot
        save_ranking_snapshot(df)
        print("\n✅ Snapshot guardado en database")
        
        # Comparar con anterior
        print("\n🔄 Cambios desde última medición:")
        for kw in KEYWORDS:
            comparison = compare_with_previous(df, kw)
            if comparison['change'] != 'NEW':
                arrow = "↑" if comparison['change_type'] == 'UP' else "↓" if comparison['change_type'] == 'DOWN' else "→"
                print(f"  {kw}: {comparison['previous_pos']:.1f} {arrow} {comparison['current_pos']:.1f}")

utils/database.py - SQLite handler

python
import sqlite3
from datetime import datetime
from config import config

def init_db():
    """Inicializa database y crea tablas"""
    conn = sqlite3.connect(config.DB_PATH)
    c = conn.cursor()
    
    # Tabla de rankings_history
    c.execute('''
        CREATE TABLE IF NOT EXISTS rankings_history (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            date TEXT,
            keyword TEXT,
            clicks INTEGER,
            impressions INTEGER,
            ctr REAL,
            position REAL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    ''')
    
    # Índice para búsquedas rápidas
    c.execute('''
        CREATE INDEX IF NOT EXISTS idx_keyword_date 
        ON rankings_history(keyword, date)
    ''')
    
    conn.commit()
    conn.close()

def save_ranking_snapshot(df):
    """Guarda snapshot de rankings en database"""
    conn = sqlite3.connect(config.DB_PATH)
    c = conn.cursor()
    
    today = datetime.now().strftime('%Y-%m-%d')
    
    for _, row in df.iterrows():
        c.execute('''
            INSERT INTO rankings_history 
            (date, keyword, clicks, impressions, ctr, position)
            VALUES (?, ?, ?, ?, ?, ?)
        ''', (
            today,
            row['keyword'],
            row['clicks'],
            row['impressions'],
            row['ctr'],
            row['position']
        ))
    
    conn.commit()
    conn.close()

def get_ranking_history(keyword, days=30):
    """Obtiene histórico de rankings para una keyword"""
    conn = sqlite3.connect(config.DB_PATH)
    
    query = '''
        SELECT date, position, clicks, impressions 
        FROM rankings_history 
        WHERE keyword = ? 
        AND date >= date('now', '-' || ? || ' days')
        ORDER BY date ASC
    '''
    
    df = pd.read_sql_query(query, conn, params=(keyword, days), parse_dates=['date'])
    conn.close()
    
    return df

Reporte 3: Landing Pages Analysis

Propósito: Identificar top páginas por clicks/impressions y encontrar oportunidades de optimización.

python
# reports/landing_pages.py
from datetime import datetime, timedelta
import pandas as pd
from auth import authenticate

def get_landing_pages_report(days=30, rows_limit=20):
    """
    Obtiene reporte de landing pages
    
    Args:
        days: Días de datos a consultar
        rows_limit: Máximo de páginas a retornar
    
    Returns:
        DataFrame con landing pages ordenadas
    """
    service = authenticate()
    
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
    
    print(f"Consultando top {rows_limit} landing pages...")
    
    request = {
        'startDate': start_date,
        'endDate': end_date,
        'dimensions': ['page'],
        'rowLimit': rows_limit,
        'type': 'web'
    }
    
    response = service.searchanalytics().query().get(body=request).execute()
    rows = response.get('rows', [])
    
    if not rows:
        print("No data encontrado")
        return None
    
    data = []
    for row in rows:
        page_url = row['keys'][0]
        
        # Extraer path limpio
        if page_url.startswith('https://'):
            path = page_url.split('/', 3)[-1]
        else:
            path = page_url
        
        data.append({
            'page': page_url,
            'path': path,
            'clicks': row['clicks'],
            'impressions': row['impressions'],
            'ctr': round(row['ctr'] * 100, 2),
            'position': round(row['position'], 2)
        })
    
    df = pd.DataFrame(data)
    df = df.sort_values('impressions', ascending=False)
    
    return df

def identify_opportunities(df):
    """
    Identifica páginas con oportunidades de optimización
    
    Criterios:
    - Alta impressions + bajo CTR (<2%)
    - Posición 11-20 (casi en top 10)
    - Alta oportunidad de mejora
    """
    if df is None:
        return []
    
    opportunities = []
    
    for _, row in df.iterrows():
        opp_score = 0
        reasons = []
        
        # Alta impressions, bajo CTR
        if row['impressions'] > 100 and row['ctr'] < 2:
            opp_score += 30
            reasons.append('Alta impresión, bajo CTR')
        
        # Posición 11-20
        if 11 <= row['position'] <= 20:
            opp_score += 20
            reasons.append('Posición 11-20 (cerca top 10)')
        
        # Buenas impresiones, posición mejorable
        if row['impressions'] > 500 and row['position'] > 5:
            opp_score += 15
            reasons.append('Buen tráfico, posición >5')
        
        if opp_score > 0:
            opportunities.append({
                'page': row['page'],
                'path': row['path'],
                'clicks': row['clicks'],
                'impressions': row['impressions'],
                'ctr': row['ctr'],
                'position': row['position'],
                'opportunity_score': opp_score,
                'reasons': reasons
            })
    
    # Ordenar por oportunidad
    opportunities.sort(key=lambda x: x['opportunity_score'], reverse=True)
    
    return opportunities

if __name__ == '__main__':
    df = get_landing_pages_report(days=90, rows_limit=50)
    
    if df is not None:
        print("\n📊 Top 10 Landing Pages (by impressions):")
        print(df[['path', 'impressions', 'clicks', 'ctr', 'position']].head(10).to_string(index=False))
        
        # Identificar oportunidades
        opportunities = identify_opportunities(df)
        
        if opportunities:
            print("\n🎯 Páginas con Oportunidades de Optimización:")
            for opp in opportunities[:10]:
                print(f"\n  {opp['path']}")
                print(f"    Score: {opp['opportunity_score']} | Pos: {opp['position']} | CTR: {opp['ctr']}%")
                print(f"    Razones: {', '.join(opp['reasons'])}")

Reporte 4: Comparativa MoM/YoY

Propósito: Comparación automática Mes-a-Mes y Año-a-Año con deltas calculados.

python
# reports/comparative.py
from datetime import datetime, timedelta
import pandas as pd
from auth import authenticate

def get_period_data(days_ago):
    """
    Obtiene datos de un período específico
    
    Args:
        days_ago: Días hacia atrás desde hoy
    
    Returns:
        DataFrame con datos agregados del período
    """
    service = authenticate()
    
    end_date = datetime.now() - timedelta(days=days_ago)
    start_date = end_date - timedelta(days=30)  # Período de 30 días
    
    request = {
        'startDate': start_date.strftime('%Y-%m-%d'),
        'endDate': end_date.strftime('%Y-%m-%d'),
        'type': 'web'
    }
    
    response = service.searchanalytics().query().get(body=request).execute()
    rows = response.get('rows', [])
    
    if not rows:
        return None
    
    # Sumar todas las filas (agregado)
    total = {
        'clicks': sum(row['clicks'] for row in rows),
        'impressions': sum(row['impressions'] for row in rows),
    }
    
    total['ctr'] = (total['clicks'] / total['impressions'] * 100) if total['impressions'] > 0 else 0
    
    return total

def calculate_comparative():
    """
    Calcula comparativa MoM y YoY
    
    Returns:
        Dict con comparativas y deltas
    """
    # Período actual (últimos 30 días)
    current = get_period_data(days_ago=30)
    
    # Período anterior (hace 30-60 días)
    previous = get_period_data(days_ago=60)
    
    # YoY (hace 365 días)
    last_year = get_period_data(days_ago=395)
    
    if not current:
        print("No data disponible")
        return None
    
    # Calcular deltas
    results = {
        'current': current,
        'previous': previous,
        'last_year': last_year,
        'mom': {},
        'yoy': {}
    }
    
    # Mes-a-Mes
    if previous:
        for metric in ['clicks', 'impressions', 'ctr']:
            current_val = current[metric]
            previous_val = previous[metric]
            
            delta = current_val - previous_val
            delta_pct = (delta / previous_val * 100) if previous_val > 0 else 0
            
            results['mom'][metric] = {
                'current': round(current_val, 2),
                'previous': round(previous_val, 2),
                'delta': round(delta, 2),
                'delta_pct': round(delta_pct, 2)
            }
    
    # Año-a-Año
    if last_year:
        for metric in ['clicks', 'impressions', 'ctr']:
            current_val = current[metric]
            last_year_val = last_year[metric]
            
            delta = current_val - last_year_val
            delta_pct = (delta / last_year_val * 100) if last_year_val > 0 else 0
            
            results['yoy'][metric] = {
                'current': round(current_val, 2),
                'previous': round(last_year_val, 2),
                'delta': round(delta, 2),
                'delta_pct': round(delta_pct, 2)
            }
    
    return results

def format_comparative_report(results):
    """Formatea reporte comparativo para visualización"""
    lines = []
    
    lines.append("="*50)
    lines.append("COMPARATIVA MoM / YoY")
    lines.append("="*50)
    
    # MoM
    if results.get('mom'):
        lines.append("\n📊 MES-A-MES (últimos 30 días vs 30 días anteriores):")
        lines.append("-")
        
        for metric, data in results['mom'].items():
            arrow = "↑" if data['delta'] > 0 else "↓" if data['delta'] < 0 else "→"
            delta_str = f"{data['delta_pct']:+.1f}%"
            
            lines.append(f"  {metric.upper()}: {data['current']:,.0f} → {data['previous']:,.0f} ({arrow} {delta_str})")
    
    # YoY
    if results.get('yoy'):
        lines.append("\n📈 AÑO-A-AÑO (vs hace 1 año):")
        lines.append("-")
        
        for metric, data in results['yoy'].items():
            arrow = "↑" if data['delta'] > 0 else "↓" if data['delta'] < 0 else "→"
            delta_str = f"{data['delta_pct']:+.1f}%"
            
            lines.append(f"  {metric.upper()}: {data['current']:,.0f} → {data['previous']:,.0f} ({arrow} {delta_str})")
    
    lines.append("\n" + "="*50)
    
    return "\n".join(lines)

if __name__ == '__main__':
    results = calculate_comparative()
    
    if results:
        report = format_comparative_report(results)
        print(report)
        
        # Guardar en archivo
        filename = f"comparative_{datetime.now().strftime('%Y%m%d')}.txt"
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(report)
        
        print(f"\n✅ Reporte guardado: {filename}")

Alertas Automatizadas

Propósito: Notificar automáticamente cuando hay cambios significativos en los datos.

python
# utils/alerts.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from config import config

def check_significant_changes(current_df, previous_df):
    """
    Compara dos períodos y detecta cambios significativos
    
    Returns:
        Lista de alertas
    """
    alerts = []
    
    # Comparar métricas agregadas
    current_clicks = current_df['clicks'].sum()
    previous_clicks = previous_df['clicks'].sum()
    
    if previous_clicks > 0:
        clicks_change_pct = (current_clicks - previous_clicks) / previous_clicks
        
        if abs(clicks_change_pct) >= config.ALERT_CLICKS_CHANGE:
            alerts.append({
                'type': 'clicks_change',
                'severity': 'high' if abs(clicks_change_pct) > 0.3 else 'medium',
                'message': f"Clicks {'+' if clicks_change_pct > 0 else ''}{clicks_change_pct:.1%} vs período anterior",
                'current_value': int(current_clicks),
                'previous_value': int(previous_clicks)
            })
    
    # Detectar nuevas keywords en top 10
    # (Esto requiere tracking histórico, pseudo-código)
    # new_top_keywords = detect_new_top_positions(current_df, previous_df)
    
    return alerts

def send_email_alert(alerts):
    """Envía alertas por email"""
    if not config.EMAIL_TO:
        print("⚠️ Email no configurado (set EMAIL_TO en .env)")
        return
    
    if not alerts:
        print("No hay alertas para enviar")
        return
    
    # Crear mensaje
    msg = MIMEMultipart()
    msg['From'] = config.EMAIL_USER
    msg['To'] = ', '.join(config.EMAIL_TO)
    msg['Subject'] = f"🚨 Alertas SEO - {len(alerts)} cambios detectados"
    
    # Cuerpo del email
    body = "<h2>Alertas de Cambios Significativos</h2>\n"
    body += "<ul>"
    
    for alert in alerts:
        severity_icon = "🔴" if alert['severity'] == 'high' else "⚠️"
        body += f"<li>{severity_icon} {alert['message']}</li>"
    
    body += "</ul>"
    body += "<p><em>Automated by GSC Reporting Framework</em></p>"
    
    msg.attach(MIMEText(body, 'html'))
    
    # Enviar
    try:
        with smtplib.SMTP(config.SMTP_SERVER, config.SMTP_PORT) as server:
            server.starttls()
            server.login(config.EMAIL_USER, config.EMAIL_PASSWORD)
            server.send_message(msg)
        print(f"✅ Email enviado a {len(config.EMAIL_TO)} destinatarios")
    except Exception as e:
        print(f"❌ Error enviando email: {e}")

# Integración en reportes
# from utils.alerts import check_significant_changes, send_email_alert
# alerts = check_significant_changes(current_df, previous_df)
# if alerts:
#     send_email_alert(alerts)

Deployment en Producción

Opción 1: Cron Jobs (Linux/Mac)

text
# Editar crontab
crontab -e

# Añadir línea para ejecutar diariamente a las 9am
0 9 * * * cd /ruta/a/gsc-reporting && python -m reports.performance >> logs/cron.log 2>&1

# Verificar cron jobs
crontab -l

Opción 2: GitHub Actions

text
# .github/workflows/gsc-report.yml
name: GSC Weekly Report
on:
  schedule:
    - cron: '0 9 * * 1'  # Lunes a las 9am
  workflow_dispatch:  # Permitir ejecución manual

jobs:
  report:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.10'
      
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      
      - name: Run performance report
        env:
          PROPERTY_URL: ${{ secrets.PROPERTY_URL }}
        run: |
          python -m reports.performance
      
      - name: Upload reports
        uses: actions/upload-artifact@v2
        with:
          name: gsc-reports
          path: '*.json,*.csv,*.html

Opción 3: Google Cloud Scheduler

python
# main.py - Modificado para Cloud Functions
def run_performance_report(request):
    """HTTP endpoint para Cloud Functions"""
    from reports.performance import get_performance_report
    
    df, summary = get_performance_report(days=7)
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'success': True,
            'summary': summary
        })
    }

# Desplegar con:
# gcloud functions deploy gsc-reporting --runtime python39 --trigger-http

Conclusión

Con este framework de reporting automatizado, tienes:

  • 4 reportes modulares: Performance, Rankings, Landing Pages, Comparativas
  • Multi-formato: JSON, CSV, HTML, email según necesites
  • Históricos: Base de datos SQLite para tracking temporal
  • Alertas: Notificaciones automáticas de cambios significativos
  • Automatización: Cron, GitHub Actions, Cloud Scheduler

Casos de uso del framework

Próximos pasos

  • Añade más dimensiones: Device (mobile/desktop), country, search appearance
  • Integra con Analytics: Combina GSC + GA4 para reportes completos
  • Dashboard visual: Conecta con Metabase/Grafana para visualización
  • Predicción: Usa históricos para predecir tendencias con scikit-learn

Recursos recomendados

  • GSC API Documentation
  • Google API Python Client
  • Cron Tutorial
  • GitHub Actions Documentation

Sobre el Autor

Alfonso Calero Gijón es Editor de 4SEO.es, portal de noticias y comunidad SEO. Especialista en automatización de procesos SEO y arquitectura de información. Escribe análisis y guías sobre automatización SEO para la comunidad.

Conectar en LinkedIn →

¿Necesitas ayuda con automatización SEO?

Sigue en 4SEO → Más guías en el blog

Blog 4SEO → Más guías en el blog

Blog 4SEO → Más guías de SEO técnico

Artículos Relacionados

  • Python para SEO: Automatiza tu Trabajo Diario
  • API REST para SEO: Integraciones y Dashboards
  • Auditoría SEO Técnica: Guía Completa 2026

Ver como Markdown