← все задачи
Async Python · задача 9 из 10
Асинхронный итератор по страницам API
Средний
15–20 минут
__aiter__генераторыленивость
Условие
Чужой API отдаёт данные страницами с курсором. Напишите асинхронный генератор, который отдаёт записи по одной, сам подгружая следующие страницы, пока они есть.
Что требуется
- Снаружи используется как async for item in fetch_items(...)
- Страницы подгружаются лениво, по мере необходимости
- Можно остановиться в любой момент, не выкачав всё
Пример
async for user in fetch_items(session, "/api/users"):
if user["id"] == target:
break # лишние страницы не грузятся
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Как устроена пагинация: курсор, offset или ссылка next в ответе?
- Есть ли ограничение по частоте запросов к API?
- Нужен ли повтор при обрыве в середине выкачки и можно ли продолжить с курсора?
Показать решение Скрыть решение
Решение
async def fetch_items(session, url, page_size=100):
cursor = None
while True:
params = {"limit": page_size}
if cursor:
params["cursor"] = cursor
async with session.get(url, params=params) as response:
response.raise_for_status()
payload = await response.json()
for item in payload["items"]:
yield item # отдаём по одной записи
cursor = payload.get("next_cursor")
if not cursor:
return
# Тот же смысл классом, если нужно хранить состояние снаружи:
class Pages:
def __init__(self, session, url):
self.session = session
self.url = url
def __aiter__(self):
return fetch_items(self.session, self.url)
Почему так
Почему генератор, а не список страниц
- Вызывающий получает записи по одной и может остановиться на первой подходящей — остальные страницы не запрашиваются
- Память не зависит от объёма данных: в ней одна страница, а не весь ответ API
- Такой интерфейс скрывает пагинацию: снаружи просто поток записей
Почему курсор лучше offset
- offset при вставках и удалениях между запросами пропускает и дублирует записи — данные едут
- В базе OFFSET 100000 заставляет сервер пролистать сто тысяч строк на каждой странице
- Курсор фиксирует позицию по отсортированному ключу — стабильно и дёшево
Что происходит при break
- Прерывание async for закрывает генератор: внутрь на текущем yield прилетает GeneratorExit
- Если внутри есть try/finally, он отработает — там и закрывают ресурсы
- Именно поэтому сессию сюда передают снаружи: генератор не должен закрывать то, чем не владеет
Что спросят дальше
- Спросят про конкурентную выкачку: страницы с курсором принципиально последовательны, распараллелить можно только диапазоны
- Спросят про дубликаты между страницами — при курсорной пагинации на равных ключах такое бывает
- Спросят про 429 в середине выкачки: нужен повтор с паузой, при этом курсор сохраняется и всё продолжается с того же места
Следующая задача
Кэш и одновременные промахи — Сто запросов одновременно не нашли значение в кэше — в базу должен уйти один.