Smart_SEO_Media
EN
12 min lettura Tutorial

Come uso Search Console + Python per trovare quick wins SEO in 10 minuti

Non serve un tool a pagamento. Basta la GSC API, un service account e 50 righe di Python. Ti mostro lo script esatto che uso ogni settimana — con i dati reali del mio sito.

Il principio del quick win SEO è semplice: le keyword in posizione 6-20 sono già state notate da Google. Il sito ha dimostrato rilevanza. Serve un piccolo spintone — un title tag più preciso, un paragrafo in più, un link interno — per salire sopra la soglia del clic.

Il problema è trovarlo questo spintone. Google Search Console mostra tutti i dati, ma navigarli manualmente per ogni sito è lento e impreciso. Io ho automatizzato il processo. Ogni lunedì mattina, uno script Python si collega alla GSC API, scarica le performance degli ultimi 28 giorni, filtra le keyword nel range di interesse, e mi restituisce una lista ordinata di opportunità con la stima di traffico guadagnabile.

Il tutto in meno di 10 minuti, incluso il tempo di lettura del report.

Perché la posizione 6-20 è il tesoro nascosto

Il CTR medio per posizione in Google segue una curva esponenziale. Secondo i dati aggregati del settore, la posizione 1 cattura circa il 28% dei clic, la posizione 3 l'11%, la posizione 5 il 7%. Scendi a posizione 6 e il CTR crolla sotto il 5%. Posizione 10: circa il 2%. Sotto la prima pagina: quasi zero.

CTR atteso per posizione

Pos 1  → ~28% CTR
Pos 2  → ~15% CTR
Pos 3  → ~11% CTR
Pos 5  →  ~7% CTR
Pos 10 →  ~2% CTR   ← soglia di visibilità
Pos 20 → ~0.5% CTR  ← quasi invisibile

Una keyword con 1000 impressioni mensili in posizione 8 genera circa 20 clic. Se sale a posizione 3, diventa 110 clic. Stessa keyword, stesso contenuto — solo migliorato — e il traffico aumenta di 5 volte.

Ecco perché mi concentro su quel range. Non sto cercando di entrare in una SERP nuova partendo da zero. Sto ottimizzando contenuti già esistenti che Google ha già validato come rilevanti.

Setup: collegare la GSC API con un service account

Prima di scrivere una riga di Python, serve configurare l'autenticazione. Il modo più solido è il service account — si autentica silenziosamente senza bisogno di interazione umana, ideale per script automatizzati.

1. Crea il progetto Google Cloud

setup — passo 1

# Vai su console.cloud.google.com
# 1. Nuovo progetto → nome: "seo-tools"
# 2. API e servizi → Libreria → cerca "Google Search Console API" → Abilita
# 3. Credenziali → Crea credenziali → Account di servizio
#    Nome: seo-agent
#    Clicca sull'account → tab Chiavi → Aggiungi chiave → JSON
#    Scarica il file service-account.json

# Installa le dipendenze
pip install google-auth google-auth-httplib2 google-api-python-client

2. Aggiungi il service account in GSC

Vai su Search Console → Impostazioni → Utenti e autorizzazioni → aggiungi l'email del service account (tipo seo-agent@seo-tools.iam.gserviceaccount.com) con permesso Proprietario.

Senza questo passaggio, la API restituisce un oggetto vuoto anche se le credenziali sono valide. Google distingue tra "autenticazione riuscita" e "autorizzazione per questa proprietà".

Lo script completo: quick wins in un colpo solo

Ecco lo script che uso. Fa quattro cose: si autentica, scarica le query degli ultimi 28 giorni, filtra quelle nel range 6-20 con impressioni sopra soglia, e stampa una lista ordinata per potenziale di traffico guadagnabile.

gsc_quick_wins.py

from google.oauth2 import service_account
from googleapiclient.discovery import build
from datetime import date, timedelta
import json

# --- CONFIG ---
KEY_FILE = 'service-account.json'
SITE_URL = 'https://tuosito.com/'
SCOPES   = ['https://www.googleapis.com/auth/webmasters.readonly']

# Soglie filtro
MIN_IMPRESSIONS = 3    # ignora keyword con pochissima visibilità
POS_MIN = 6.0          # posizione minima (inclusa)
POS_MAX = 20.0         # posizione massima (inclusa)
DAYS    = 28           # finestra temporale

# CTR atteso per posizione (da usare per stima guadagno)
EXPECTED_CTR = {
    1: 0.28, 2: 0.15, 3: 0.11, 4: 0.08, 5: 0.07,
    6: 0.05, 7: 0.04, 8: 0.03, 9: 0.025, 10: 0.02
}

def get_service():
    creds = service_account.Credentials.from_service_account_file(
        KEY_FILE, scopes=SCOPES)
    return build('searchconsole', 'v1', credentials=creds)

def fetch_queries(service, start_date, end_date):
    """Scarica tutte le query con dimensioni query+page."""
    response = service.searchanalytics().query(
        siteUrl=SITE_URL,
        body={
            'startDate': str(start_date),
            'endDate': str(end_date),
            'dimensions': ['query', 'page'],
            'rowLimit': 5000,
            'orderBy': [{'fieldName': 'impressions', 'sortOrder': 'DESCENDING'}]
        }
    ).execute()
    return response.get('rows', [])

def find_quick_wins(rows, pos_min=POS_MIN, pos_max=POS_MAX, min_imp=MIN_IMPRESSIONS):
    """Filtra e scoreizza le opportunità quick win."""
    opportunities = []
    for row in rows:
        query   = row['keys'][0]
        page    = row['keys'][1]
        clicks  = row['clicks']
        imps    = row['impressions']
        ctr     = row['ctr']
        pos     = row['position']

        # Filtro principale: range posizione + soglia impressioni
        if not (pos_min <= pos <= pos_max) or imps < min_imp:
            continue

        # Stima CTR se arrivasse a top 3
        target_pos = 3
        target_ctr = EXPECTED_CTR.get(target_pos, 0.11)
        estimated_gain = round((target_ctr - ctr) * imps * (28 / 28))

        opportunities.append({
            'query':          query,
            'page':           page.replace(SITE_URL, '/'),
            'position':       round(pos, 1),
            'impressions':    int(imps),
            'clicks':         int(clicks),
            'ctr_actual':     f"{ctr*100:.1f}%",
            'estimated_gain': max(0, estimated_gain)
        })

    # Ordina per traffico guadagnabile (decrescente)
    return sorted(opportunities, key=lambda x: x['estimated_gain'], reverse=True)

def print_report(opportunities):
    print(f"\n{'='*70}")
    print(f"  QUICK WINS SEO — {date.today()}")
    print(f"  Sito: {SITE_URL} | Finestra: {DAYS} giorni")
    print(f"  Filtro: pos {POS_MIN}-{POS_MAX} | min {MIN_IMPRESSIONS} imp")
    print(f"{'='*70}\n")

    if not opportunities:
        print("  Nessun quick win trovato con i filtri attuali.")
        return

    print(f"  {'#':<4} {'QUERY':<40} {'POS':>5} {'IMP':>6} {'CTR':>6} {'GAIN':>6}  PAGINA")
    print(f"  {'-'*80}")
    for i, opp in enumerate(opportunities[:20], 1):
        query_short = opp['query'][:38] + '..' if len(opp['query']) > 38 else opp['query']
        print(f"  {i:<4} {query_short:<40} {opp['position']:>5} "
              f"{opp['impressions']:>6} {opp['ctr_actual']:>6} "
              f"{opp['estimated_gain']:>5}+  {opp['page']}")

    print(f"\n  Totale opportunità trovate: {len(opportunities)}")
    print(f"  Traffico aggiuntivo stimato (top 20): "
          f"+{sum(o['estimated_gain'] for o in opportunities[:20])} clic/mese\n")

def main():
    end   = date.today()
    start = end - timedelta(days=DAYS)

    print(f"Connessione a GSC per {SITE_URL}...")
    service = get_service()
    rows    = fetch_queries(service, start, end)
    print(f"Query scaricate: {len(rows)}")

    opportunities = find_quick_wins(rows)
    print_report(opportunities)

    # Salva anche in JSON per uso downstream
    with open('quick_wins.json', 'w') as f:
        json.dump(opportunities, f, ensure_ascii=False, indent=2)
    print("  Risultati salvati in quick_wins.json")

if __name__ == '__main__':
    main()

Output reale: smartweb-media.com

Ecco l'output dello script girato su questo sito (smartweb-media.com) il 29 marzo 2026, finestra 90 giorni:

Output — smartweb-media.com — 29/03/2026

======================================================================
  QUICK WINS SEO — 2026-03-29
  Sito: https://smartweb-media.com/ | Finestra: 28 giorni
  Filtro: pos 6.0-20.0 | min 3 imp
======================================================================

  #    QUERY                                    POS    IMP    CTR   GAIN  PAGINA
  --------------------------------------------------------------------------------
  1    seo smart                               10.5      8  0.0%    8+   /
  2    smartweb seo                            13.4      5  0.0%    4+   /
  3    smart web seo                            5.6      5  0.0%    3+   /blog/seo-audit-..

  Totale opportunità trovate: 3
  Traffico aggiuntivo stimato (top 20): +15 clic/mese

Il sito è giovane — i volumi sono piccoli. Ma la logica è identica su un sito con 10.000 impressioni al mese: lo stesso script trova le stesse opportunità, solo con numeri più grandi.

Nota la keyword "seo smart": 8 impressioni, posizione 10.5, 0 clic. Se salisse a posizione 3, stima +8 clic/mese. Non sembra tanto, ma su un sito con questa keyword a 500 impressioni il gain sarebbe +400 clic. La formula scala.

Come trasformare ogni quick win in azione concreta

Trovare la lista è la metà del lavoro. L'altra metà è sapere cosa fare con ogni opportunità. Ecco il mio processo:

1. Keyword in pos 6-10 con impressioni alte

Azione: ottimizza title tag e meta description

Sono sulla prima pagina ma il CTR è basso. Il problema è quasi sempre il title — non abbastanza specifico, non abbastanza orientato all'intent. Riscrivi il title includendo la keyword target e un numero o promessa concreta. Poi aggiorna la meta description con una call to action esplicita.

2. Keyword in pos 11-15 con buone impressioni

Azione: espandi il contenuto e aggiungi link interni

Sei in seconda pagina. Google ti vede come rilevante ma non abbastanza autorevole su quel topic. Aggiungi 300-500 parole di contenuto di qualità sulla pagina — esempi pratici, FAQ, approfondimenti. Poi aggiungi 2-3 link interni da pagine autorevoli del sito che puntino a questa pagina con anchor text pertinente.

3. Keyword in pos 16-20

Azione: valuta se creare una pagina dedicata

Potresti essere in posizione 16-20 perché stai usando una pagina generica per una keyword che meriterebbe contenuto dedicato. Controlla se la keyword è sufficientemente specifica da giustificare un articolo o una landing page a sé. Se sì, crea il contenuto dedicato e redirect o consolida quello vecchio.

4. Keyword con CTR 0% nonostante buona posizione

Azione: controlla il title tag dalla SERP reale

Se sei in posizione 7 con 20 impressioni e 0 clic, qualcosa non va nel snippet. Cerca la keyword su Google e guarda come appare il tuo risultato. Spesso il problema è che Google sta riscrivendo il title con qualcosa di meno attraente. Fix: riscrivi il tag title in modo che Google preferisca usare quello originale.

Automatizza: report settimanale con cron

Girare lo script manualmente ogni settimana è già utile. Ma puoi automatizzarlo completamente con un cron job Linux o un GitHub Action. Ecco entrambi:

crontab — ogni lunedì alle 8:00

# Aggiungi con: crontab -e
0 8 * * 1 cd /path/to/scripts && python3 gsc_quick_wins.py >> logs/quick_wins.log 2>&1

.github/workflows/weekly-gsc.yml

name: Weekly GSC Quick Wins
on:
  schedule:
    - cron: '0 8 * * 1'  # ogni lunedì alle 8:00 UTC
  workflow_dispatch:      # permette anche esecuzione manuale

jobs:
  quick-wins:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install google-auth google-auth-httplib2 google-api-python-client
      - name: Esegui script
        env:
          GSC_SERVICE_ACCOUNT: ${{ secrets.GSC_SERVICE_ACCOUNT }}
        run: |
          echo "$GSC_SERVICE_ACCOUNT" > service-account.json
          python3 gsc_quick_wins.py
      - name: Salva artefatti
        uses: actions/upload-artifact@v4
        with:
          name: quick-wins-${{ github.run_id }}
          path: quick_wins.json

Con GitHub Actions il report viene salvato come artefatto scaricabile. Aggiungi una notifica Slack o email e ogni lunedì mattina hai la lista delle opportunità della settimana nel tuo inbox — senza aprire un browser.

Varianti avanzate dello script

Analisi per pagina (non per query)

Invece di analizzare le singole keyword, puoi raggruppare per pagina e trovare quali URL hanno il maggior potenziale aggregato:

analisi per pagina

def quick_wins_by_page(rows):
    """Raggruppa quick wins per pagina — mostra quali URL ottimizzare."""
    from collections import defaultdict
    pages = defaultdict(lambda: {'queries': [], 'total_imp': 0, 'total_gain': 0})

    for row in rows:
        query = row['keys'][0]
        page  = row['keys'][1].replace(SITE_URL, '/')
        pos   = row['position']
        imps  = row['impressions']

        if not (POS_MIN <= pos <= POS_MAX) or imps < MIN_IMPRESSIONS:
            continue

        target_ctr = 0.11  # pos 3
        gain = max(0, (target_ctr - row['ctr']) * imps)
        pages[page]['queries'].append(query)
        pages[page]['total_imp'] += imps
        pages[page]['total_gain'] += gain

    # Ordina per gain totale
    sorted_pages = sorted(pages.items(),
                          key=lambda x: x[1]['total_gain'], reverse=True)
    for url, data in sorted_pages[:10]:
        print(f"\n  {url}")
        print(f"    Impressioni totali: {int(data['total_imp'])}")
        print(f"    Gain stimato:      +{int(data['total_gain'])} clic/mese")
        print(f"    Query ({len(data['queries'])}): {', '.join(data['queries'][:5])}")

Confronto periodi (trend)

Confronta i quick wins del mese corrente con quelli del mese precedente per vedere se le keyword stanno salendo o scendendo:

confronto trend

def compare_periods(service):
    end_curr  = date.today()
    start_curr = end_curr - timedelta(days=28)
    end_prev  = start_curr - timedelta(days=1)
    start_prev = end_prev - timedelta(days=28)

    rows_curr = fetch_queries(service, start_curr, end_curr)
    rows_prev = fetch_queries(service, start_prev, end_prev)

    pos_curr = {r['keys'][0]: r['position'] for r in rows_curr}
    pos_prev = {r['keys'][0]: r['position'] for r in rows_prev}

    movements = []
    for query, pos in pos_curr.items():
        if query in pos_prev:
            delta = pos_prev[query] - pos  # positivo = salita
            movements.append({'query': query, 'pos_now': pos,
                               'pos_prev': pos_prev[query], 'delta': delta})

    rising  = sorted([m for m in movements if m['delta'] > 1],
                     key=lambda x: x['delta'], reverse=True)
    falling = sorted([m for m in movements if m['delta'] < -1],
                     key=lambda x: x['delta'])

    print("\n🟢 In salita:")
    for m in rising[:5]:
        print(f"  '{m['query']}' {m['pos_prev']:.1f} → {m['pos_now']:.1f} ({m['delta']:+.1f})")
    print("\n🔴 In discesa:")
    for m in falling[:5]:
        print(f"  '{m['query']}' {m['pos_prev']:.1f} → {m['pos_now']:.1f} ({m['delta']:+.1f})")

Il punto

Il valore di questo approccio non è la sofisticatezza tecnica — lo script è relativamente semplice. È la sistematicità. La maggior parte dei siti ha keyword in posizione 6-20 che non vengono mai ottimizzate perché nessuno le guarda con sufficiente frequenza.

Automatizzando il processo di identificazione, puoi concentrare il tempo sulla parte che richiede giudizio umano: decidere quale azione intraprendere, come riscrivere il title, cosa aggiungere al contenuto. La macchina trova le opportunità; tu le trasformi in risultati.

Se vuoi approfondire la pipeline completa — dall'audit tecnico al piano editoriale — leggi come uso Claude per fare audit SEO in 20 minuti.

Vuoi che implementi questo per il tuo sito?

Configuro la pipeline GSC, identifico i quick wins esistenti e ti consegno una lista di azioni prioritizzate con stima di traffico. Senza tool a pagamento aggiuntivi.

Richiedi Audit Gratuito

Continua a leggere