Smart_SEO_Media
EN
10 min lettura Automazione SEO

GSC + Python: lo script per monitorare keyword in posizione 6-20

Setup completo dell'autenticazione GSC API con service account, script di estrazione keyword, calcolo potenziale di clic, export CSV e automazione settimanale. Tutto quello che serve per trasformare GSC in un sistema di monitoraggio attivo.

Google Search Console è gratuita, aggiornata quotidianamente, e contiene le keyword più accurate per il tuo sito — perché vengono direttamente da Google, non da un panel di clickstream o stime di terze parti.

Il problema è l'interfaccia. È pensata per esplorazioni manuali, non per monitoraggio sistematico. Non puoi impostare alert su keyword specifiche. Non puoi confrontare periodi in modo automatico. Non puoi ricevere un report settimanale con le opportunità più calde.

Tutto questo si fa in Python, in 100 righe di codice, una volta sola. Ti mostro come.

Prerequisiti: setup autenticazione

Per interrogare la GSC API hai bisogno di un service account Google Cloud con accesso alla tua proprietà Search Console. Se non l'hai ancora configurato, leggi il setup completo. In sintesi:

setup rapido

# 1. Google Cloud Console → Nuovo progetto → API e servizi → Libreria
#    Cerca "Google Search Console API" → Abilita

# 2. Credenziali → Crea credenziali → Account di servizio
#    Scarica il file JSON → salvalo come service-account.json

# 3. Search Console → Impostazioni → Utenti e autorizzazioni
#    Aggiungi: nome@progetto.iam.gserviceaccount.com → Proprietario

# 4. Installa librerie
pip install google-auth google-auth-httplib2 google-api-python-client pandas

Lo script completo di monitoraggio

Questo script fa tutto: si autentica, scarica le keyword degli ultimi 28 giorni e del periodo precedente, calcola i movimenti, identifica le opportunità in posizione 6-20, stima il potenziale di traffico e salva tutto in un CSV con timestamp:

gsc_keyword_monitor.py

"""
GSC Keyword Monitor v1.0
Monitora keyword in posizione 6-20, calcola potenziale di clic,
confronta con periodo precedente, esporta CSV.
"""

from google.oauth2 import service_account
from googleapiclient.discovery import build
from datetime import date, timedelta
from pathlib import Path
import pandas as pd
import json
import os

# ============================================================
# CONFIGURAZIONE
# ============================================================
CONFIG = {
    'key_file':    'service-account.json',
    'site_url':    'https://smartweb-media.com/',
    'scopes':      ['https://www.googleapis.com/auth/webmasters.readonly'],
    'days':        28,           # finestra temporale
    'pos_min':     6.0,          # posizione minima quick wins
    'pos_max':     20.0,         # posizione massima quick wins
    'min_imps':    3,            # impressioni minime da includere
    'output_dir':  'gsc_reports',
    'row_limit':   5000,
}

# CTR benchmark per posizione (stime conservative)
CTR_BENCHMARK = {
    1: 0.28, 2: 0.15, 3: 0.11, 4: 0.09, 5: 0.07,
    6: 0.05, 7: 0.04, 8: 0.03, 9: 0.025, 10: 0.02,
    11: 0.018, 12: 0.015, 13: 0.013, 14: 0.012, 15: 0.010,
    16: 0.009, 17: 0.008, 18: 0.007, 19: 0.006, 20: 0.005,
}

# ============================================================
# AUTENTICAZIONE
# ============================================================
def get_service(key_file, scopes):
    """Crea il client GSC autenticato con service account."""
    creds = service_account.Credentials.from_service_account_file(
        key_file, scopes=scopes)
    return build('searchconsole', 'v1', credentials=creds)

# ============================================================
# ESTRAZIONE DATI
# ============================================================
def fetch_search_analytics(service, site_url, start_date, end_date,
                           dimensions=None, row_limit=5000):
    """Scarica dati Search Analytics dalla GSC API."""
    if dimensions is None:
        dimensions = ['query', 'page']

    response = service.searchanalytics().query(
        siteUrl=site_url,
        body={
            'startDate': str(start_date),
            'endDate': str(end_date),
            'dimensions': dimensions,
            'rowLimit': row_limit,
            'orderBy': [{'fieldName': 'impressions', 'sortOrder': 'DESCENDING'}]
        }
    ).execute()
    return response.get('rows', [])

def rows_to_dataframe(rows):
    """Converte le righe GSC in un DataFrame pandas."""
    if not rows:
        return pd.DataFrame()

    records = []
    for row in rows:
        record = {k: v for k, v in zip(['query', 'page'], row['keys'])}
        record.update({
            'clicks':      row.get('clicks', 0),
            'impressions': row.get('impressions', 0),
            'ctr':         row.get('ctr', 0),
            'position':    row.get('position', 0)
        })
        records.append(record)
    return pd.DataFrame(records)

# ============================================================
# ANALISI QUICK WINS
# ============================================================
def calculate_potential(df, pos_min, pos_max, min_imps, ctr_benchmark):
    """Identifica quick wins e calcola il potenziale di traffico."""
    # Filtra il range di posizioni
    mask = (
        (df['position'] >= pos_min) &
        (df['position'] <= pos_max) &
        (df['impressions'] >= min_imps)
    )
    wins = df[mask].copy()

    if wins.empty:
        return wins

    # Posizione target per il calcolo del gain (top 3)
    target_pos = 3
    target_ctr = ctr_benchmark.get(target_pos, 0.11)

    # Calcola gain stimato (clic aggiuntivi al mese se arriva a pos 3)
    wins['target_ctr']      = target_ctr
    wins['current_ctr_pct'] = (wins['ctr'] * 100).round(2)
    wins['gain_clic_stimato'] = (
        (target_ctr - wins['ctr']) * wins['impressions']
    ).clip(lower=0).round(0).astype(int)

    # Score opportunità: pesa impressioni + gap CTR + vicinanza top 10
    wins['proximity_score'] = (10 - wins['position'].clip(upper=10)) / 10
    wins['opp_score'] = (
        wins['gain_clic_stimato'] * 0.5 +
        wins['impressions'] * 0.3 +
        wins['proximity_score'] * 100 * 0.2
    ).round(1)

    return wins.sort_values('opp_score', ascending=False)

# ============================================================
# CONFRONTO PERIODI
# ============================================================
def compare_periods(curr_df, prev_df):
    """Confronta posizioni attuali con periodo precedente."""
    if curr_df.empty or prev_df.empty:
        return curr_df

    # Posizione media per query nel periodo corrente
    curr_pos = curr_df.groupby('query')['position'].mean().rename('pos_curr')
    prev_pos = prev_df.groupby('query')['position'].mean().rename('pos_prev')

    comparison = pd.concat([curr_pos, prev_pos], axis=1).dropna()
    comparison['delta_pos'] = (comparison['pos_prev'] - comparison['pos_curr']).round(1)
    comparison['trend'] = comparison['delta_pos'].apply(
        lambda x: '↑ Salendo' if x > 0.5 else ('↓ Scendendo' if x < -0.5 else '→ Stabile')
    )
    return comparison.reset_index()

# ============================================================
# EXPORT CSV
# ============================================================
def export_reports(quick_wins, comparison, output_dir, site_url):
    """Esporta i report in CSV con timestamp."""
    Path(output_dir).mkdir(exist_ok=True)
    today = date.today().isoformat()

    # Quick wins
    if not quick_wins.empty:
        qw_file = f"{output_dir}/quick_wins_{today}.csv"
        cols = ['query', 'page', 'position', 'impressions', 'clicks',
                'current_ctr_pct', 'gain_clic_stimato', 'opp_score']
        available = [c for c in cols if c in quick_wins.columns]
        quick_wins[available].to_csv(qw_file, index=False)
        print(f"  Quick wins salvati: {qw_file}")

    # Comparazione periodi
    if not comparison.empty:
        cmp_file = f"{output_dir}/movements_{today}.csv"
        comparison.to_csv(cmp_file, index=False)
        print(f"  Movimenti salvati:  {cmp_file}")

    # Summary JSON
    summary = {
        'date': today,
        'site': site_url,
        'quick_wins_count': len(quick_wins),
        'total_gain_potential': int(quick_wins['gain_clic_stimato'].sum()) if not quick_wins.empty else 0,
        'top_opportunities': quick_wins.head(5)[['query','position','gain_clic_stimato']].to_dict('records') if not quick_wins.empty else []
    }
    with open(f"{output_dir}/summary_{today}.json", 'w') as f:
        json.dump(summary, f, ensure_ascii=False, indent=2)

# ============================================================
# MAIN
# ============================================================
def main():
    cfg = CONFIG
    end_curr  = date.today()
    start_curr = end_curr - timedelta(days=cfg['days'])
    end_prev  = start_curr - timedelta(days=1)
    start_prev = end_prev - timedelta(days=cfg['days'])

    print(f"GSC Keyword Monitor — {cfg['site_url']}")
    print(f"Periodo corrente:   {start_curr} → {end_curr}")
    print(f"Periodo precedente: {start_prev} → {end_prev}\n")

    # Autenticazione
    service = get_service(cfg['key_file'], cfg['scopes'])

    # Scarica dati
    print("Scaricando dati periodo corrente...")
    rows_curr = fetch_search_analytics(service, cfg['site_url'],
                                       start_curr, end_curr,
                                       row_limit=cfg['row_limit'])
    print(f"  {len(rows_curr)} righe scaricate")

    print("Scaricando dati periodo precedente...")
    rows_prev = fetch_search_analytics(service, cfg['site_url'],
                                       start_prev, end_prev,
                                       row_limit=cfg['row_limit'])
    print(f"  {len(rows_prev)} righe scaricate\n")

    # Converti in DataFrame
    df_curr = rows_to_dataframe(rows_curr)
    df_prev = rows_to_dataframe(rows_prev)

    # Analisi quick wins
    quick_wins = calculate_potential(
        df_curr, cfg['pos_min'], cfg['pos_max'],
        cfg['min_imps'], CTR_BENCHMARK
    )

    # Confronto periodi
    comparison = compare_periods(df_curr, df_prev)

    # Stampa report in console
    print(f"{'='*65}")
    print(f"QUICK WINS (pos {cfg['pos_min']}-{cfg['pos_max']}, min {cfg['min_imps']} imp)")
    print(f"{'='*65}")
    if quick_wins.empty:
        print("  Nessun quick win trovato con i filtri correnti.")
    else:
        print(f"  {'QUERY':<38} {'POS':>5} {'IMP':>6} {'GAIN':>6}  PAGINA")
        print(f"  {'-'*75}")
        for _, row in quick_wins.head(15).iterrows():
            q = row['query'][:36] + '..' if len(row['query']) > 36 else row['query']
            page = row.get('page', '').split('.com')[-1] or '/'
            print(f"  {q:<38} {row['position']:>5.1f} {int(row['impressions']):>6} "
                  f"{row['gain_clic_stimato']:>5}+  {page}")
        print(f"\n  Totale: {len(quick_wins)} opportunità")
        print(f"  Gain totale stimato: +{quick_wins['gain_clic_stimato'].sum()} clic/mese")

    if not comparison.empty:
        rising  = comparison[comparison['delta_pos'] > 1].head(5)
        falling = comparison[comparison['delta_pos'] < -1].head(5)
        if not rising.empty:
            print(f"\n🟢 IN SALITA:")
            for _, row in rising.iterrows():
                print(f"   '{row['query']}' {row['pos_prev']:.1f}→{row['pos_curr']:.1f} ({row['delta_pos']:+.1f})")
        if not falling.empty:
            print(f"\n🔴 IN DISCESA:")
            for _, row in falling.iterrows():
                print(f"   '{row['query']}' {row['pos_prev']:.1f}→{row['pos_curr']:.1f} ({row['delta_pos']:+.1f})")

    # Export
    print(f"\nExport report...")
    export_reports(quick_wins, comparison, cfg['output_dir'], cfg['site_url'])
    print("\nDone.")

if __name__ == '__main__':
    main()

Personalizzare i filtri

Il dizionario CONFIG all'inizio dello script permette di adattare il monitoraggio al tuo sito senza toccare la logica:

configurazioni consigliate per tipo di sito

Sito nuovo (<6 mesi):
  pos_min: 11, pos_max: 30, min_imps: 2, days: 90

Sito consolidato (1-3 anni):
  pos_min: 6,  pos_max: 20, min_imps: 10, days: 28

E-commerce grande:
  pos_min: 4,  pos_max: 15, min_imps: 50, days: 14

Blog/editoriale:
  pos_min: 6,  pos_max: 25, min_imps: 5,  days: 28

Automazione: report settimanale automatico

Con un cron job, lo script gira ogni lunedì e salva il report nella cartella gsc_reports/. Aggiungi notifica email o Telegram per riceverlo direttamente:

crontab — automazione settimanale

# Ogni lunedì alle 8:00 — genera report e invia notifica Telegram
0 8 * * 1 cd /path/to/scripts && python3 gsc_keyword_monitor.py && \
  python3 -c "
import json, requests
from pathlib import Path
from datetime import date

summary_file = f'gsc_reports/summary_{date.today()}.json'
if Path(summary_file).exists():
    data = json.load(open(summary_file))
    msg = f'📊 GSC Weekly Report\n'
    msg += f'Sito: {data[\"site\"]}\n'
    msg += f'Quick wins: {data[\"quick_wins_count\"]}\n'
    msg += f'Gain stimato: +{data[\"total_gain_potential\"]} clic/mese\n\n'
    for opp in data[\"top_opportunities\"][:3]:
        msg += f'  • {opp[\"query\"]} (pos {opp[\"position\"]:.1f}) → +{opp[\"gain_clic_stimato\"]} clic\n'

    BOT_TOKEN = 'IL_TUO_BOT_TOKEN'
    CHAT_ID   = 'IL_TUO_CHAT_ID'
    requests.post(f'https://api.telegram.org/bot{BOT_TOKEN}/sendMessage',
                  json={'chat_id': CHAT_ID, 'text': msg})
"

Come leggere il report CSV

Lo script genera tre file per ogni run:

quick_wins_YYYY-MM-DD.csv

Colonne: query, page, position, impressions, clicks, current_ctr_pct, gain_clic_stimato, opp_score. Ordinato per opp_score decrescente = le opportunità migliori in cima.

movements_YYYY-MM-DD.csv

Confronto posizione corrente vs periodo precedente. Colonne: query, pos_curr, pos_prev, delta_pos, trend. Utile per vedere cosa sta salendo e cosa sta scendendo.

summary_YYYY-MM-DD.json

Riassunto testuale con conteggi e top 5 opportunità. Utile per integrare con dashboard o notifiche automatiche.

Il punto sul monitoraggio vs l'azione

Il monitoraggio è inutile senza azione. Lo script ti dice dove sono le opportunità — ma il lavoro reale è decidere cosa fare per ogni keyword identificata.

Per ogni quick win trovato, il mio processo è sempre lo stesso:

  1. Apri la pagina che ranka per quella keyword
  2. Controlla il title tag — è specifico per questa keyword?
  3. Controlla la meta description — ha una CTA chiara?
  4. Leggi il contenuto — risponde esattamente a quello che cerca chi digita questa query?
  5. Controlla i link interni — ci sono pagine autorevoli del sito che potrebbero linkare a questa?

Cinque domande. Le risposte indicano il fix. Lo script trova le opportunità; tu decidi come sfruttarle.

Vuoi che imposto questo sistema per il tuo sito?

Configuro il monitoraggio GSC, identifico le prime opportunità e ti consegno una lista di azioni prioritizzate con stima di traffico guadagnabile.

Richiedi Audit Gratuito

Continua a leggere