Cuatro formas de pedir la página siguiente, la fila que se lee dos veces, y el 403 que no trae Retry-After y por tanto tu reintentador ignora.
⏱ 15 min de lectura
Las dos cosas de esta lección van juntas porque se causan mutuamente: una API pagina para que no le pidas cien mil filas de golpe, y pone un límite de uso para que no se las pidas en cien mil peticiones seguidas.
Y las dos son la causa habitual de que un programa que funcionaba en pruebas con veinte registros se rompa en producción con veinte mil.
Es más limpio, porque el cuerpo son solo datos. Y requests la analiza por su cuenta:
python
respuesta.links['next']['url']
Por cursor. Te dan una marca opaca y la devuelves para seguir. Es lo que hace GitHub ahora, dentro de esa cabecera:
Salida en pantalla
after = Y3Vyc29yOnYyOpLPAAABoMmoDUjPAAAAAUpL110=
descodificado -> b'cursor:v2:\x92\xcf\x00\x00\x01\xa0...'
Empieza por cursor:v2: y sigue en binario. Es opaco a propósito: no está pensado para que lo interpretes, sino para que lo devuelvas tal cual.
Fíjate en lo que no hay en esa cabecera:
Salida en pantalla
¿hay rel="last"? False
GitHub tenía un enlace a la última página cuando paginaba por número, y con cursor no puede: no se sabe cuántas páginas hay sin recorrerlas. Ese es el precio.
La paginación por número tiene un fallo que no se ve en pruebas y se ve siempre en producción: los datos se mueven mientras los recorres.
Pides las filas 1 a 20. Mientras las procesas, alguien inserta una fila nueva que va la primera. Pides las filas 21 a 40:
Salida en pantalla
antes: [nueva] A B C ... T | U V W ...
└─ página 1 ─┘ └─ página 2
la fila T era la 20 y ahora es la 21 -> la lees DOS veces
Y al revés: si alguien borra una fila, la que estaba en el límite se salta y no la lees nunca. Sin error, sin aviso, con 200 en todas las peticiones.
Para un listado que mira una persona, da igual. Para un proceso que sincroniza datos cada noche, es que faltan registros y hay duplicados, y descubrir por qué lleva días.
El cursor no tiene ese problema porque dice "dame lo que va después de esta fila", no "dame las veinte siguientes por número". Si insertan algo antes, no afecta. Es exactamente la paginación por cursor de la lección 36 del módulo anterior, ahora desde el lado del cliente.
Y el otro motivo es el coste, que también se midió allí: con desplazamiento, la base de datos tiene que recorrer y tirar las filas anteriores, así que la página mil tarda mil veces más.
La forma correcta es seguir el enlace que te dan, no construirlo:
python
defrecorrer(sesion, url,**kwargs):while url:
respuesta = sesion.get(url, timeout=(3.05,15),**kwargs)
respuesta.raise_for_status()yieldfrom respuesta.json()
url = respuesta.links.get('next',{}).get('url')
kwargs.pop('params',None)# el enlace ya los trae dentro
Los parámetros se quitan tras la primera vuelta. El enlace de la cabecera ya trae dentro el tamaño de página, los filtros y el cursor. Volver a pasarlos puede duplicarlos o pisar el cursor, que es el fallo que deja el bucle recorriendo la primera página para siempre.
Es un generador. Devuelve filas según llegan en vez de acumularlas, así que se puede recorrer un millón de registros sin tenerlos todos en memoria. Es la respuesta al límite de la lección 3.
Se sigue el enlace, no se inventa. Si mañana GitHub cambia de número a cursor —cosa que ya ha hecho— el código sigue funcionando. El que construía ?page=N+1 a mano, no.
Para las APIs que no mandan cabecera y ponen la navegación en el cuerpo, el mismo bucle con la clave correspondiente:
python
url = datos.get('next')# Django REST Framework
url = datos.get('next_page_url')# otras
Toda API seria los tiene, y suelen ser de tres tipos combinados: por hora, por segundo, y de concurrencia. Los dos últimos se olvidan y son los que muerden cuando alguien decide paralelizar.