Парсинг данных через API на Python: лимиты, пагинация, дубли
Парсинг данных через API — это когда вы забираете информацию не со страницы, а из штатного интерфейса сервиса: запрос, JSON, понятные поля. Сложность здесь не в первом запросе (он пишется за десять минут), а в том, что происходит на трёхсотой тысяче записей: ключи, лимиты, пагинация и повторные запуски, которые норовят удвоить данные. Ниже — как это устроено и где обычно ломается.
Чем официальный API лучше парсинга HTML
Если у источника есть открытый API, спорить не о чем: брать данные надо оттуда. Парсер HTML читает то, что нарисовано для человека, и ломается от смены дизайна. API отдаёт то, что предназначено для программы, и меняется по правилам.
Есть и бонус, о котором вспоминают поздно: у API почти всегда есть фильтр по дате изменения. Повторная выгрузка тянет не весь каталог, а только изменившееся со вчера — разница между сорока минутами и четырьмя секундами.
Обратная сторона честная: API отдаёт только то, что решил отдать вендор. Нужного поля в ответе нет — придётся запрашивать расширенный доступ или добирать данные другим путём.
Парсинг данных API на Python: из чего состоит клиент
Разница между «скриптом с requests» и рабочей интеграцией — в слоях, отвечающих за поведение при сбоях. Библиотека тут почти не важна: requests для простого случая, httpx — когда нужны асинхронные запросы.
- Таймаут обязателен на каждом запросе. По умолчанию клиент готов ждать вечно — однажды вы получите намертво зависшую задачу в планировщике.
- Повторять можно не всё. GET безопасно всегда. Повтор POST после таймаута — способ создать вторую сделку, поэтому такие запросы либо идемпотентны по ключу, либо не повторяются сами.
- Ответ надо проверять, а не доверять ему. Пустой массив вместо ошибки, строка вместо числа, null в обязательном поле — обычное дело.
Ключи и лимиты: как не поймать 429
Почти любой публичный API ограничивает частоту запросов, и разница между анонимным и авторизованным доступом огромна. Пример — REST API GitHub: без ключа 60 запросов в час, с персональным токеном 5000. Отсюда первое действие перед выгрузкой: получить ключ, даже если данные публичные.
Когда лимит всё же выбран, сервис отвечает кодом 429 Too Many Requests, а иногда сразу говорит, сколько ждать, в заголовке Retry-After. Многие API дополнительно присылают остаток квоты в заголовках вида X-RateLimit-Remaining и время её сброса. Правильный клиент читает эти заголовки и притормаживает сам, а не долбит сервер повторами.
- Держать свой лимит ниже заявленного. Разрешено 10 запросов в секунду — ходите на 6–7: у чужого счётчика своя логика округления.
- Уважать Retry-After. Пришёл заголовок — ждём ровно столько, сколько сказали.
- Повторять с экспоненциальной задержкой и случайной добавкой. 1, 2, 4, 8 секунд плюс разброс: без него параллельные задачи синхронно ударят по серверу в один момент.
- Различать 4xx и 5xx. 500-е — временная проблема на их стороне, повтор оправдан. 400-е (кроме 429) значат, что запрос неправильный, повторять бессмысленно.
- Ключ хранить вне кода. Переменные окружения или секрет-хранилище: токен, попавший в репозиторий, отзывается — иногда автоматически и в самый неудобный момент.
Отдельно про квоты: у части сервисов лимит считается не в запросах, а в «стоимости» — сложный запрос списывает больше. Это всегда написано в документации и всегда обнаруживается на второй день работы теми, кто её не прочитал.
Пагинация: почему offset врёт
API почти никогда не отдаёт всё сразу — данные приходят страницами. Способов разбить их на страницы три, и они принципиально разного качества.
| Тип | Как выглядит | Чем опасен |
|---|---|---|
| Offset / page | ?page=3&per_page=100 | При вставках и удалениях между запросами записи дублируются или пропадают; на глубоких страницах резко замедляется |
| Cursor | ?cursor=eyJpZCI6... | Курсор нельзя собрать вручную и нельзя прыгнуть на середину — только последовательный обход |
| Keyset | ?updated_after=2026-08-01&id_gt=15320 | Требует стабильной сортировки, зато устойчив к изменениям данных |
Классическая ошибка: выгрузка идёт полчаса, за это время в источник добавляют записи, содержимое сдвигается на страницу — и часть данных вы не увидели, а часть скачали дважды. Именно поэтому Stripe, Slack и GitHub используют курсоры. И если сервис отдаёт ссылку на следующую страницу сам (GitHub кладёт её в заголовок Link с признаком rel="next"), идти надо по ней, а не конструировать адрес самому.
И правило, которое стоит дороже всего: выгрузка завершена не тогда, когда кончились ожидаемые страницы, а когда сервер не вернул следующий курсор. Всё остальное — тихая потеря хвоста данных.
Пока читаете — можно сразу проверить свою задачу. Опишите процесс, и я скажу, решается ли он и во сколько обойдётся.
Как не собрать дубли при повторном запуске
Любая интеграция запускается повторно: сеть отвалилась, задачу перезапустили, понадобилось догрузить прошлый месяц. Требование звучит просто — повторный запуск за тот же период должен давать тот же результат, а не второй комплект строк. Достигается это тремя приёмами.
Ключ вместо порядкового номера
У каждой записи есть естественный идентификатор из источника: id заказа, номер документа, ИНН. Загрузка делается не вставкой, а upsert — «обнови по этому ключу, а если записи нет — создай». В PostgreSQL это INSERT ... ON CONFLICT (external_id) DO UPDATE. Уникальный индекс здесь важнее самого запроса: он физически не даст появиться дублю, даже если две задачи запустятся одновременно.
Инкремент с перехлёстом
Хранится отметка, до какого момента данные забраны. Следующий запуск просит записи, изменённые после неё, но с запасом назад — на несколько минут или час. Это закрывает расхождение часов между серверами и записи, не зафиксированные в момент прошлой выгрузки. Повторы при этом безвредны ровно потому, что загрузка идёт через upsert.
Идемпотентность на запись
Когда интеграция не только читает, но и создаёт объекты на чужой стороне, вопрос встаёт зеркально. Хороший API даёт штатный механизм: в Stripe это заголовок Idempotency-Key — строка до 255 символов (рекомендуется UUID v4), по которой сервер узнаёт повтор и возвращает результат первого запроса вместо создания второго платежа. Ключи живут 24 часа, а тот же ключ с другими параметрами вернёт ошибку. Если механизма нет, его роль играет ваш собственный внешний номер в создаваемом объекте.
Что делать, когда API нет
Ситуация обычная: данные нужны, публичного API нет. Порядок действий от дешёвого к дорогому такой.
- Спросить. У множества сервисов API есть, но не вынесен в публичную документацию — доступ дают по запросу или на партнёрском тарифе. Письмо не стоит ничего и часто закрывает вопрос.
- Поискать фид и экспорт. Прайс в YML, выгрузка в CSV по ссылке, отчёты на почту по расписанию — у поставщиков это встречается чаще, чем API, и им самим выгодно, чтобы ваши цены были актуальны.
- Посмотреть, чем питается сам сайт. Современный интерфейс почти всегда получает данные отдельными запросами к внутренним эндпоинтам с JSON — это видно в панели разработчика на вкладке сети. Удобнее HTML, но обязательств у такого эндпоинта нет: он может измениться без предупреждения, и считать его контрактом нельзя.
- Парсить HTML. Последний вариант по надёжности, но рабочий — со своими правилами вежливости и юридическими рамками. Разобрано в статье про парсер сайтов.
- Взять посредника. Для популярных источников есть агрегаторы с собственным API: дороже за объём, зато чужая забота о поломках.
Собранное обычно едет не в файл, а в базу или CRM — эта часть разобрана в статье про интеграцию с CRM. Если же результат нужен людям в привычном виде, поверх базы делается выгрузка в таблицу: про это — в разборе автоматизации Python и Excel.
Сроки, цена и когда заказывать не нужно
| Задача | Что входит | Срок и цена |
|---|---|---|
| Разовая выгрузка | Один эндпоинт, пагинация, результат в CSV или таблицу | 1–2 дня, от 15 000 ₽ |
| Регулярная интеграция | Ключи, лимиты, инкремент, upsert в базу, расписание, уведомления | 5–10 дней, 30 000–90 000 ₽ |
| Несколько источников со сверкой | 3–10 API, приведение справочников, история изменений | от 150 000 ₽ |
| Поддержка | Реакция на смену версии API и новые поля | от 8 000 ₽ в месяц |
А теперь часть, из-за которой я иногда отговариваю от разработки. Интеграция не нужна, если:
- Данные нужны один раз. Справочник на 2000 позиций для разового анализа — это готовый экспорт из личного кабинета и полчаса времени.
- У сервиса есть штатная выгрузка по расписанию. Многие системы сами шлют отчёт на почту или в облачную папку — бесплатно и уже работает.
- Есть готовый коннектор. Если источник и приёмник уже соединяются типовым модулем платформы, своя интеграция окупится только при нестандартной логике.
- Объём мал и не растёт. Двадцать заявок в день переносятся руками быстрее, чем обсуждается техзадание.
- Нет ответственного за данные. Если никто не знает, какой из двух справочников главный, интеграция просто ускорит расхождение.
Если данные нужны ежедневно, источников больше одного и на них завязаны деньги — интеграция окупается за первые недели. Основная работа там не в запросах, а в трёх вещах: лимиты, пагинация, повторные запуски. Они и отличают выгрузку, которая живёт год, от скрипта, сломавшегося на второй неделе.
Частые вопросы
Чем парсинг через API лучше парсинга сайта?
API отдаёт данные в структурированном виде с заявленным контрактом полей и не ломается от смены дизайна. У него есть легальная квота, фильтр по дате изменения и предсказуемое поведение при ошибках. Парсинг HTML — вариант для случаев, когда API нет вообще.
Что делать, если API отдаёт ошибку 429?
Это превышение лимита частоты. Нужно посмотреть заголовок Retry-After и подождать указанное время, а в клиенте заранее держать скорость ниже разрешённой и повторять запросы с экспоненциальной задержкой и случайной добавкой. Долбить повторами бесполезно — счётчик от этого только растёт.
Как выгрузить все данные, если API отдаёт их страницами?
Идти по пагинации до конца и определять конец не по числу страниц, а по отсутствию следующего курсора или ссылки next. Если сервис поддерживает курсорную пагинацию, использовать её: постраничный offset при изменении данных во время выгрузки теряет и дублирует записи.
Как избежать дублей при повторном запуске выгрузки?
Загружать данные не вставкой, а upsert по естественному ключу из источника (id заказа, номер документа) с уникальным индексом в базе. Тогда повторный запуск за тот же период обновит существующие строки, а не создаст вторые. Для записи в чужой сервис используется идемпотентный ключ запроса.
Нужен ли ключ, если данные публичные?
Как правило да, и он резко расширяет лимиты. У GitHub, например, анонимно доступно 60 запросов в час против 5000 с токеном. Ключ хранится в переменных окружения или секрет-хранилище, а не в коде.
Сколько стоит интеграция с API?
Разовая выгрузка из одного эндпоинта — от 15 000 ₽ и 1–2 дня. Регулярная интеграция с лимитами, инкрементальной догрузкой и загрузкой в базу — 30 000–90 000 ₽ за 5–10 дней. Сборка из нескольких источников со сверкой справочников — от 150 000 ₽.
Читайте дальше
Нужны данные из чужого сервиса?
Скажите, что за источник и куда должны приезжать данные — проверю, есть ли у него API, и назову срок и цену выгрузки.
Обсудить задачу