Gestire la paginazione delle API con Python Requests

by theArchitect
SHARE
Gestire la paginazione delle API con Python Requests
© Guida-HTML5.it

Introduzione

Molte API non restituiscono tutti i dati in una sola risposta. Quando una risorsa contiene migliaia di elementi, il server suddivide il risultato in più pagine per ridurre il carico e migliorare i tempi di risposta. Questo meccanismo è chiamato paginazione.

Un esempio tipico è un endpoint che restituisce un elenco di utenti, ordini o prodotti. Invece di ricevere subito l’intero archivio, il client può ottenere 20 elementi alla volta usando parametri come page, per_page o limit. Alcune API usano invece un cursore, ad esempio next_cursor, che indica da quale elemento continuare.

Gestire correttamente la paginazione è importante perché consente di:

  • limitare la quantità di dati trasferiti in una singola richiesta;
  • evitare di caricare in memoria risposte troppo grandi;
  • elaborare i risultati progressivamente;
  • rendere il codice compatibile con API diverse;
  • interrompere il recupero quando sono disponibili abbastanza dati.

In questo tutorial costruiremo un client Python basato su requests per leggere tutte le pagine di un’API paginata. Useremo una struttura simile a quella presente in molti servizi reali: una lista di risultati e un campo next contenente l’URL della pagina successiva.

Codice completo

import requests
from typing import Iterator, Dict, Any


def scorri_risorse(
    url_iniziale: str,
    params: dict | None = None
) -> Iterator[Dict[str, Any]]:
    """
    Restituisce una risorsa alla volta seguendo i link di paginazione.

    L´API deve restituire una risposta simile a:
    {
        "data": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Marco"}
        ],
        "links": {
            "next": "https://api.example.com/users?page=2"
        }
    }
    """

    url_corrente = url_iniziale
    parametri_correnti = params.copy() if params else None

    while url_corrente:
        risposta = requests.get(
            url_corrente,
            params=parametri_correnti,
            headers={"Accept": "application/json"}
        )

        # Solleva un´eccezione per risposte HTTP non valide
        risposta.raise_for_status()

        payload = risposta.json()

        # Recupera gli elementi della pagina corrente
        elementi = payload.get("data", [])

        for elemento in elementi:
            yield elemento

        # Dopo la prima richiesta, il link successivo contiene già
        # tutti i parametri necessari.
        parametri_correnti = None
        url_corrente = payload.get("links", {}).get("next")


def main() -> None:
    url = "https://api.example.com/users"
    parametri = {
        "limit": 100,
        "sort": "name"
    }

    numero_elementi = 0

    try:
        for utente in scorri_risorse(url, parametri):
            print(f"{utente[´id´]}: {utente[´name´]}")
            numero_elementi += 1

            # Esempio: interrompere il recupero dopo 250 elementi
            if numero_elementi == 250:
                print("Limite locale raggiunto.")
                break

    except requests.RequestException as errore:
        print(f"Errore durante la richiesta HTTP: {errore}")
    except ValueError as errore:
        print(f"La risposta non contiene JSON valido: {errore}")


if __name__ == "__main__":
    main()

Spiegazione

Perché usare un generatore

La funzione scorri_risorse() usa la parola chiave yield. In questo modo non costruisce una grande lista contenente tutti i risultati, ma restituisce un elemento alla volta.

Questa scelta è particolarmente utile quando l’API restituisce molti dati. Con una lista tradizionale, il programma potrebbe consumare molta memoria:

tutti_gli_utenti = []

# Tutti gli elementi vengono conservati in memoria
for utente in scorri_risorse(url):
    tutti_gli_utenti.append(utente)

Con un generatore, invece, è possibile elaborare immediatamente ogni risultato, salvarlo su un database o scriverlo su un file senza attendere la fine della paginazione.

Seguire il link della pagina successiva

Il ciclo while continua finché url_corrente contiene un valore. Dopo aver elaborato la pagina, il codice legge:

url_corrente = payload.get("links", {}).get("next")

Se l’API restituisce un URL valido, il ciclo procede con una nuova richiesta. Quando viene raggiunta l’ultima pagina, il campo next può essere null, assente oppure vuoto. In tutti questi casi il ciclo termina.

Paginazione basata su numero di pagina

Non tutte le API forniscono un link completo. Alcune restituiscono informazioni come page, total_pages e per_page. In questo caso si può incrementare manualmente il numero della pagina:

import requests

def recupera_pagine(url: str, dimensione_pagina: int = 50) -> list:
    risultati = []
    pagina = 1

    while True:
        risposta = requests.get(
            url,
            params={
                "page": pagina,
                "per_page": dimensione_pagina
            }
        )
        risposta.raise_for_status()

        dati = risposta.json()
        elementi = dati.get("items", [])
        risultati.extend(elementi)

        # L´ultima pagina contiene meno elementi della dimensione richiesta
        if len(elementi) < dimensione_pagina:
            break

        pagina += 1

    return risultati

Questa tecnica è semplice, ma non sempre è affidabile: alcuni server possono restituire pagine parziali anche quando esistono ancora altri dati. Quando possibile, è preferibile usare un campo esplicito come has_next o un link next.

Paginazione con cursore

Le API più moderne utilizzano spesso un cursore. Il server restituisce un token da inviare nella richiesta successiva:

import requests

def recupera_con_cursore(url: str):
    cursore = None

    while True:
        params = {"limit": 100}

        if cursore:
            params["cursor"] = cursore

        risposta = requests.get(url, params=params)
        risposta.raise_for_status()

        dati = risposta.json()

        for elemento in dati.get("items", []):
            yield elemento

        cursore = dati.get("next_cursor")

        if not cursore:
            break

Il cursore è spesso più stabile della numerazione delle pagine, soprattutto quando i dati cambiano mentre vengono letti. Se nuovi elementi vengono inseriti nel database, infatti, la paginazione numerica può causare duplicati o omissioni.

Best practice

  • Leggere sempre la documentazione dell’API: controlla il nome dei campi, la dimensione massima della pagina e il formato del link successivo.
  • Impostare una dimensione ragionevole: una pagina troppo piccola aumenta il numero di richieste; una pagina troppo grande può rallentare il server e aumentare la memoria utilizzata.
  • Usare raise_for_status(): una risposta HTTP con codice 400 o 500 non dovrebbe essere trattata come una risposta valida.
  • Validare la struttura JSON: non dare per scontato che data, items o links siano sempre presenti.
  • Imporre un limite locale: quando si esegue un’importazione, un numero massimo di elementi evita cicli troppo lunghi o errori dovuti a dati inattesi.
  • Evitare loop infiniti: se un’API restituisce sempre lo stesso link next, è utile memorizzare gli URL già visitati e interrompere l’elaborazione in caso di ripetizione.
  • Elaborare i dati progressivamente: il pattern con yield è ideale per pipeline, esportazioni e sincronizzazioni.
  • Considerare i dati modificati durante la lettura: per sincronizzazioni importanti, preferisci cursori o filtri basati su timestamp quando l’API li supporta.

Una protezione aggiuntiva contro una paginazione difettosa può essere implementata così:

url_visitati = set()

while url_corrente:
    if url_corrente in url_visitati:
        raise RuntimeError("L´API ha restituito un link di paginazione ripetuto")

    url_visitati.add(url_corrente)

    # Eseguire qui la richiesta e leggere il link successivo
    url_corrente = prossimo_link

Riepilogo

La paginazione è un aspetto fondamentale nella gestione delle API con requests. Le strategie più comuni sono la paginazione tramite numero di pagina, tramite link e tramite cursore. Il pattern con un generatore permette di recuperare ed elaborare gli elementi uno alla volta, evitando di caricare l’intero risultato in memoria.

Un client robusto deve conoscere il formato della risposta, seguire correttamente la pagina successiva, gestire le risposte HTTP non valide e prevedere limiti per evitare elaborazioni incontrollate. La scelta tra pagine numeriche e cursori dipende dal comportamento e dalla documentazione del servizio utilizzato.

Approfondisci con risorse ufficiali

SHARE