С завидным постоянством в тематических каналах и чатах возникают вопросы про 429 и 500 ошибки при получении данных из API WB или OZ. Коллеги интересуются, нет ли особого параметра, секретного метода или “золотого” часа, когда гарантированно можно получить данные без ошибок. Увы, нет. А техподдержка отвечает всегда примерно так: "на метод высокая нагрузка, настройте ретраи на своей стороне". Поделюсь своим опытом решения этой проблемы, который применяется в системе аналитики продаж через Wildberries и Ozon. Напомню, небольшой обзор своей системы WBOZYA-dash я делал в предыдущей статье.
Очевидный прием
Самый очевидный путь решения проблемы частых ошибок 429 и 50x - относиться как к штатной ситуации и соответственно обрабатывать программной логикой. А логика тут одна - снова и снова делать запрос. Только чтобы не попасть под какие-либо санкции со стороны API, желательно ввести самоцензуру - паузы между запросами, ограничение на количество перезапросов.
Функции опроса API с обработкой ошибок может выглядеть так:
def get_oz(url, heads, params, max_retries=5, timeout=30): result = {} for retries in range(1, max_retries + 1): try: response = requests.post(url, headers=heads, json=params, timeout=timeout) except requests.RequestException as e: print(f'!!! Сетевая ошибка в попытке {retries}: {e}!!!') time.sleep(2 * retries) continue # Код 200 получен правильный ответ if response.status_code == 200: return response.json() # Код 429 - Много запросов, делаем умную паузу и снова запрашиваем elif response.status_code == 429: pause_time = 2 ** retries # X-Ratelimit-Retry - фишка WB, но может и OZ когда нить введет if 'X-Ratelimit-Retry' in response.headers: pause_time = int(response.headers['X-Ratelimit-Retry']) + 5 print(f'get_oz: ! -Много запросов- в попытке {retries}. Пауза {pause_time} сек') time.sleep(pause_time) # При ошибке 5xx через паузу уходим на следующую попытку elif 500 <= response.status_code < 600: pause_time = 2 ** retries print(f'get_oz: ! -Ошибка {response.status_code}- в попытке {retries}. Пауза {pause_time} сек') time.sleep(pause_time) # Если другая ошибка - выходим из цикла! else: print(f'get_oz: !!! Ошибка получения данных по API! code: {response.status_code}') return None print(f'get_oz: !!! Все {max_retries} попытки провалились') return None
Стоит отметить, что эта функция только на один запрос. То есть цикл с перебором страниц через offset или page будет на уровень выше в основной процедуре логики получения данных из API. Обычно 5 попыток хватает, если нет - то, видимо, на сервере серьезная перегрузка, и брать его измором "здесь и сейчас" наверняка не имеет смысла. Стоит возобновить запросы сильно попозже - например, через час. Как это сделать - покажу ниже по тексту.
Неочевидный прием
А пока расскажу о неочевидном пути решения: уменьшать объем получаемой информации. Логика тут такая при меньшем объеме данных, меньше и вероятность получить ошибку. Например, если запрашивать, скажем, транзакции OZ за неделю, в ответе будет 10-20 тысяч строк, а метод API все равно отдает их по 1000, то есть надо будет запросить 20 или более страниц. И вот где-то на 16-ой мы получаем ошибку и скаченные ранее 15 тысяч строк идут в /dev/null. Надо все заново перезапрашивать.
Предлагаю применять следующую тактику: запросить поменьше, сохранить полученные данные и запросить следующую партию. В том же примере про транзакции OZ, если за раз запрашивать транзакции только за один день, а всю неделю получить в цикле перебором семи независимых самостоятельных запросов.
Пример кода для запроса и сохранения транзакций по дням:
date_from = today() - timedelta(days=7) date_to = today() - timedelta(days=1) current_date = date_from delta = timedelta(days=1) #перебираем дни от date_from до date_to while current_date <= date_to: transactions_oneday = pd.DataFrame([]) url = "https://api-seller.ozon.ru/v3/finance/transaction/list" ipage = 1 all_pages_data = [] while True: data = { "filter": { "date": { "from": str(current_date) + "T00:00:00.000Z", "to": str(current_date) + "T23:59:59.999Z" }, }, "page": ipage, "page_size": 1000 } dd = [] apioz_response = get_oz(url, headers, data, 5) if len(apioz_response) > 0 : dd = apioz_response['result']['operations'] all_pages_data.extend(dd) else: print(f'!!! Пустой ответ от API') # если на странице меньше 1000 строк - она последняя, выходим из цикла if len(dd) < 1000: break ipage +=1 transactions_oneday = pd.DataFrame(all_pages_data) #сразу экспортируем в базу, что получили из API export_to_db('oz_transactions', transactions_oneday) #переходим к следующему дню current_date += delta
Получая 1-3 страницы данных, вероятность словить ошибку очевидно меньше, чем 15-20 страниц. Правда, это будет уже 4 или 5 уровень циклирования, но все же мой опыт показывает, что такой подход эффективен.
Костыль
Увы, очевидный и не очевидные пути все еще не дают полной гарантии получения информации из API. Сервис может просто не работать и измором и сотнями ретраев его не поднять. Тогда вступает черед следующего пути - назову его "костыль" =)
Если опрос API по расписанию где-то в 6-7 утра в итоге после всех ретраев так и не позволил загрузить целостный массив данных, что обычно делаем - загружаем в ручную. Так почему бы не запрограммировать и это. Написать еще одну процедуру, которая также периодически будет анализировать результаты последнего запуска нужной процедуры опроса API, и если она завершилась с ошибкой.- запускать ее снова. Или анализировать целостность сохраненных в БД данных, и перезапрашивать чего не хватает, например. транзакций за четверг.
Запускать такой мониторинг очевидно надо после завершения штатной работы целевой процедуры. Крутить его круглые сутки, наверно, особого смысла тоже нет. Лучшая тактика: запуск раз в час с 8 до 12 часов если основной опрос API происходит в 6--7 утра.
Заключение
Не скажу, что после внедрения всех этих решений, мне теперь совсем не приходится в ручную запускать опрос API, но все же делать это приходится значительно реже. В подавляющем большинстве система отрабатывает экспорт данных из API МП штатно. Но и специфика моей системы такова, что получение данных происходит всего один раз в сутки. Возможно, если у вас логика бизнес-процессов подразумевает более частый опрос API, то и нештатные ситуации будут встречаться чаще.
