Робота з API

Та сама кавʼярня, той самий прайс — але цього разу сервер віддає не сторінку для очей, а дані для програм. Що з ними робити далі: розбирати, створювати, показувати ключ, брати порціями й не зловживати чужим часом.

У попередній темі ми розібрали сам протокол: як складається запит, що означають коди станів, навіщо заголовки й таймаут. Уміння вже є — requests.get(...) приносить відповідь. Лишилося головне питання: що в цій відповіді лежить. Бо сторінка, зроблена для людських очей, і дані, зроблені для програми, — це дві дуже різні речі, хоч приходять вони однаковим способом.

Наскрізний приклад — той самий, що і в темі про CSV і JSON: маленька кавʼярня з меню на пʼять позицій. Тільки тепер меню не лежить у файлі поруч, а живе на сервері, і ми ходимо по нього по мережі.

Чому цей сервер — свій, а не чужий. Учитися на живому публічному API — погана ідея, і причина не в складності. Чужий сервіс змінює відповіді, вводить нові ліміти, вимагає реєстрації, лягає на годину й рано чи пізно зникає зовсім; той самий код завтра дасть інші числа, і ти не знатимеш, чи помилився ти, чи змінився сервіс. Тому навчальний сервер піднімають на власному компʼютері, за адресою 127.0.0.1. Це не заглушка й не імітація: requests робить справжнє TCP-зʼєднання, отримує справжні коди станів і справжні заголовки. Відрізняється лише адреса — і те, що результат буде той самий і сьогодні, і за рік, і без інтернету взагалі.
Одразу про слово. API (application programming interface, програмний інтерфейс) — це домовленість про те, як одна програма звертається до іншої. Слово ширше за мережу: у list.append() теж API. Але в цій темі йдеться саме про вебʼAPI — інтерфейс, до якого ходять по HTTP і який відповідає даними, а не версткою.

МотивСторінка для очей, дані для програм

Уяви, що кавʼярня має сайт із меню. Ти відкриваєш його — і бачиш заголовок, назви напоїв, ціни в гривнях, гарний шрифт. Усе це прийшло від сервера одним шматком тексту: HTML. Браузер прочитав розмітку й намалював сторінку.

Тепер уяви, що меню потрібне програмі: щоранку рахувати середню ціну кави й записувати її в таблицю. Програмі не потрібні шрифти, відступи й слово «грн» — їй потрібні назва і число. Але в HTML вони заховані між тегами: щоб дістати ціну, доведеться знайти потрібний тег, вирізати текст, відрізати «грн» і перетворити рядок на число. Чотири кроки там, де вистачило б двох — розібрати відповідь і взяти поле.

Саме тому в багатьох сервісів є друга адреса — та, що віддає ті самі дані у форматі, призначеному для програм. Найчастіше це JSON. Подивись, наскільки різна робота виходить в одному й другому випадку:

Одні дані, дві обгортки

Ліворуч — те, що надіслав сервер. Праворуч — що доведеться зробити, щоб дістати ціну еспресо.

усього байтів—
корисних—
кроків до числа—
Головне: корисних байтів в обох випадках однаково — це ті самі назви й ті самі ціни. Різниця в обгортці й у тому, скільки роботи вона додає. І ще в одному: верстку сторінки дизайнер може змінити завтра, і твій код зламається. Формат відповіді API так просто не міняють — на нього спираються чужі програми.

Ось звідки береться правило, з якого починається вся тема: якщо для потрібних даних є API — бери API. Розбирати сторінку доводиться лише тоді, коли API немає; цим ми займемось у наступній темі.

Розбірr.json() і де він падає

Відповідь від сервера завжди приходить як послідовність байтів. requests дає до неї три доступи: r.content — сирі байти, r.text — ті самі байти, перетворені на рядок, і r.json() — рядок, розібраний як JSON у звичайні обʼєкти Python.

Третій — це рівно те, що ми робили в темі 22 функцією json.loads(), тільки без зайвого рядка. Метод бере r.text, віддає його розбирачу JSON і повертає результат: словник, список, число — залежно від того, що надіслав сервер.

import requests

r = requests.get(f"{BASE}/menu", params={"page": 1}, timeout=5)
data = r.json()                    # текст відповіді стає словником

print(type(data))                  # <class 'dict'>
print(data["total"])               # 5
print(data["items"][0]["name"])    # Еспресо
print(data["items"][0]["price"] + 5)  # 30 — це вже число, не рядок

Останній рядок і є вся суть. З HTML ти отримав би рядок "25 грн", і додати до нього пʼятірку не вийшло б — Python кинув би TypeError. З JSON число приходить числом, бо у формату є типи: рядок у лапках, число без лапок, true і false, null. Розбирач їх розрізняє й повертає str, int, bool і None.

Пастка: сервер відповів не тим, чого ти чекав

r.json() нічого не перевіряє наперед. Він просто пробує розібрати текст — і якщо текст не JSON, кидає виняток:

r = requests.get(f"{BASE}/menu.html", timeout=5)
r.json()
requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Повідомлення виглядає загадково, а насправді воно точне: розбирач стояв на першому символі першого рядка й чекав початку значення JSON, а побачив <. Найчастіша причина саме така: сервер віддав HTML замість даних. Не з підступності — просто щось пішло не так, і замість відповіді API прилетіла сторінка помилки: «404, такої адреси немає», «502, шлюз не відповідає», сторінка входу в систему. Усе це — коректні HTTP-відповіді, тільки з версткою всередині.

Тому не сподівайся, а перевіряй. Сервер сам каже, що він надіслав, — у заголовку Content-Type:

kind = r.headers.get("Content-Type", "")

if kind.startswith("application/json"):
    data = r.json()
    print("позицій у відповіді:", len(data["items"]))
else:
    # не гадаємо, а друкуємо, що саме прийшло — це і є діагностика
    print("сервер віддав не JSON, а", kind)
    print(r.text[:120])

Другий варіант — обгорнути виклик у try, як ми робили в темі про винятки. Підходи не конкурують: Content-Type каже, чого чекати, а try ловить випадок, коли заголовок обіцяв JSON, а тіло виявилося обірваним. У серйозному коді роблять обидва.

Не плутай два «порожньо». Відповідь із кодом 204 No Content тіла не має взагалі — і r.json() на ній теж впаде, хоч сервер відпрацював ідеально. Перед розбором корисно глянути не лише на тип, а й на довжину: if r.status_code != 204 and r.content:.

БудоваРесурс і метод

Тепер про те, як API влаштовані зсередини. Найпоширеніший стиль зветься REST (representational state transfer). За ним стоїть багато теорії, але для роботи достатньо однієї ідеї, і вона напрочуд проста:

іменник — у шляху, дієслово — у методі

Тобто адреса називає ресурс — річ, про яку йдеться: /menu — усе меню, /menu/2 — друга позиція, /orders/4417/items — позиції замовлення 4417. А те, що з цією річчю робити, каже метод запиту: GET, POST, PUT, PATCH, DELETE. Адреси на кшталт /getMenu або /deleteItem?id=2 теж трапляються, але це вже інший стиль — там дієслово запхали в шлях, і метод перестає щось означати.

Два слова, які треба знати про методи, звучать страшно й означають дуже конкретні речі.

Безпечний (safe) метод нічого не змінює на сервері. Ти можеш викликати його скільки завгодно разів, і світ лишиться таким самим. Безпечний тільки GET (а ще HEAD і OPTIONS). Побутова аналогія: подивитись на цінник — безпечно; узяти товар із полиці — ні.

Ідемпотентний (idempotent) метод можна повторити, і стан лишиться тим самим, що після першого разу. Це не те саме, що безпечний: видалення змінює світ, але видалити ту саму позицію двічі — те саме, що видалити один раз, її просто вже немає. Аналогія: вимикач світла в положенні «вимкнено» — скільки не тисни, світло далі вимкнене. А от POST — це кнопка «додати ще одну позицію»: натиснув двічі — отримав дві.

Метод і наслідок

Той самий запит надсилаємо один раз і другий. Дивись на третю колонку — саме вона розрізняє методи.

код відповіді—
позицій—
Що читати: POST — єдиний, у якого друга колонка не дорівнює третій: повтор створює другий запис. Саме тому мережевий збій на POST небезпечніший за збій на будь-якому іншому методі. І зверни увагу на DELETE: відповідь другого разу інша (404 замість 204), а стан меню той самий — ідемпотентність саме про стан, а не про код.
методщо робитьбезпечнийідемпотентний
GETпрочитати ресурстактак
POSTстворити новий ресурсніні
PUTзамінити ресурс цілкомнітак
PATCHзмінити частину полівніяк домовиться сервіс
DELETEвидалити ресурснітак

Обережне формулювання в рядку PATCH стоїть чесно: стандарт його ідемпотентності не гарантує, і все вирішує тіло запиту. Якщо тіло каже «постав ціну 50» — повтор нічого не змінить. Якщо тіло каже «додай до ціни 5» — кожен повтор зробить дорожче. Формат тіла придумує сервіс, тому за PATCH завжди читай документацію.

СтворенняСтворення запису

Дотепер ми тільки читали. Створення нового запису виглядає так: метод POST, адреса колекції (не окремого елемента), а нові дані — у тілі запиту.

r = requests.post(f"{BASE}/menu",
                  json={"name": "Какао", "price": 40},
                  timeout=5)

print(r.status_code)             # 201
print(r.headers["Location"])     # /menu/99
print(r.json())                  # {'id': 99, 'name': 'Какао', 'price': 40, ...}

Три речі в цій відповіді варті окремої уваги.

Код 201 Created, а не 200 OK. Обидва означають успіх, але 201 каже більше: зʼявився новий ресурс. Якщо сервер відповів 200, найімовірніше нічого не створилося, а тобі просто щось повернули.

Заголовок Location. У ньому адреса щойно створеного ресурсу. Це важливо: id вигадує сервер, а не ти, і Location — офіційний спосіб його дізнатися. Тіло відповіді теж зазвичай містить створений обʼєкт, але не в кожного API.

Аргумент json=, а не data=. Це різні речі, і плутанина між ними — одна з найчастіших причин відповіді 400 на цілком правильні дані:

json= — тіло у форматі JSON
requests.post(url,
  json={"name": "Какао"})

тіло запиту:
{"name": "Как..."}

заголовок став сам:
Content-Type: application/json
data= — тіло як поля форми
requests.post(url,
  data={"name": "Какао"})

тіло запиту:
name=%D0%9A%D0%B0%D0%BA%D0%B0...

заголовок став сам:
Content-Type: application/x-www-
form-urlencoded

json= робить дві дії однією: перетворює словник Python на текст JSON і ставить заголовок Content-Type: application/json. Другу половину легко проґавити, і саме через неї сервер, який чекав JSON, відповідає «не розумію тіло». data= зі словником кодує його як поля HTML-форми — так надсилають дані звичайні сторінки з полями вводу, і деякі API справді хочуть саме цього. Дізнатись, котрий варіант потрібен, можна лише з документації.

ВідмоваКоли сервер каже «ні»

Спробуймо створити позицію без ціни:

r = requests.post(f"{BASE}/menu", json={"name": "Какао"}, timeout=5)

print(r.status_code)   # 422
print(r.json())        # {'error': 'бракує обовʼязкових полів', 'missing': ['price']}

Коди станів ми розбирали в попередній темі, тут важлива одна межа, і вона проходить між четвіркою й пʼятіркою.

4xx — помилка на твоєму боці. Запит дійшов, сервер його зрозумів і відмовився виконувати: бракує поля, невірний токен, немає такого ресурсу. Повторювати такий запит без змін безглуздо — відповідь буде та сама завтра й через рік. Треба виправити запит.

5xx — помилка на боці сервера. Запит був нормальний, але щось зламалося там: перевантаження, впала база даних, оновлення посеред дня. Твій код тут ні до чого, і той самий запит через кілька секунд цілком може спрацювати. Тут повтор доречний — про нього окремий розділ нижче.

Код 422 Unprocessable Content — типова відповідь на «синтаксично все гаразд, а по суті ні»: тіло є, це справжній JSON, але поля price в ньому немає. Дехто на це саме відповідає 400 Bad Request; різницю задає сервіс.

Тіло помилки треба читати. Найпоширеніша помилка новачка — подивитись на код і зупинитись. А сервер у тілі відповіді часто пише рівно те, чого бракує: {'missing': ['price']}. Один print(r.text) замінює годину гадання. Особливо це стосується r.raise_for_status(): він кидає виняток із кодом і адресою, але тіло у виняток не потрапляє — надрукуй його сам перед тим, як піднімати помилку далі.

КлючАвтентифікація й токен

Меню кавʼярні відкрите всім. А от виторг за день — ні. Щоб API впустив тебе до закритої частини, треба довести, хто ти. Найпоширеніший спосіб у вебʼAPI — токен: довгий випадковий рядок, який сервіс видає один раз і який ти прикладаєш до кожного запиту в заголовку:

headers = {"Authorization": "Bearer demo-token"}

r = requests.get(f"{BASE}/secret", headers=headers, timeout=5)
print(r.status_code)   # 200
print(r.json())        # {'revenue': 12450, 'orders': 217}

Слово Bearer («предʼявник») — це схема автентифікації, а не частина твого ключа. Воно означає буквально: хто предʼявив цей рядок, той і має доступ. Ніяких додаткових перевірок — тому токен і є секретом, рівносильним паролю.

Без заголовка або з чужим токеном сервер відмовить, але по-різному, і різницю між двома кодами варто затямити раз і назавжди:

Токен: два коди відмови й три місця витоку

Угорі — що сервер відповість. Унизу — куди потрапить сам токен, залежно від того, як ти його передаєш.

код—
місць витоку—
Головне: 401 означає «я не знаю, хто ти» — сервер не отримав або не впізнав ключ. 403 означає «я знаю, хто ти, і тобі сюди не можна» — ключ чинний, але прав бракує. Плутати їх дорого: у першому випадку треба виправляти ключ, у другому — просити доступ, і виправлення ключа не допоможе ніколи.

Чому токен не пишуть у код

Рядок token = "demo-token" посеред файлу зручний рівно доти, доки код не поїхав кудись іще: у репозиторій, у резервну копію, у скріншот для колеги. А git памʼятає історію — прибрати ключ наступним комітом не досить, він лишається в попередніх.

Робочий спосіб — тримати секрет поза кодом, у змінній оточення:

import os

token = os.environ["CAFE_TOKEN"]     # впаде з KeyError, якщо змінної немає
headers = {"Authorization": f"Bearer {token}"}

Змінну задають перед запуском: export CAFE_TOKEN=demo-token у Linux і macOS, $env:CAFE_TOKEN="demo-token" у PowerShell. Щоб не набирати це щоразу, ключі складають у файл .env поруч із проєктом — а сам .env вписують у .gitignore, щоб він ніколи не потрапив у репозиторій. Це два різні файли й дві різні дії: перший зберігає секрет, другий забороняє його комітити.

Токен у адресі світиться найяскравіше. Деякі API дозволяють ?token=… прямо в URL — і це найгірший із варіантів. Адресу цілком записує в свій журнал і сервер, і кожен проксі по дорозі; вона потрапляє в заголовок Referer, в історію браузера й в історію команд оболонки. Заголовки в журнали зазвичай не пишуть — тому Authorization і є правильним місцем.

ПорціїПагінація

Наше меню з пʼяти позицій сервер віддає по дві за раз. Виглядає дивно — доки не уявиш API з мільйоном записів. Віддати мільйон одним шматком означає зібрати його в памʼяті сервера й передати кількасот мегабайтів по мережі, а якщо зʼєднання обірветься на середині — починати спочатку. Тому дані віддають сторінками.

r = requests.get(f"{BASE}/menu", params={"page": 1}, timeout=5)
print(r.json())
{'page': 1, 'total': 5, 'next': 2,
 'items': [{'id': 1, 'name': 'Еспресо', 'price': 25}, ...]}

Крім самих items, у відповіді є службові поля: скільки записів усього (total) і яка сторінка наступна (next). На останній сторінці next дорівнює null, тобто в Python — None. Це і є сигнал зупинитись.

Скільки буде сторінок і запитів

Покрути обидва повзунки: кількість запитів залежить від розміру сторінки різкіше, ніж здається.

сторінок—
на останній—
Що читати: кожна сторінка — це окремий HTTP-запит з окремим рукостисканням і окремою затримкою мережі. Тому розмір сторінки — це компроміс: великий економить запити, але робить кожну відповідь важчою й довшою. Крайні положення повзунка показують обидві біди.

Обхід усіх сторінок — це цикл, який іде, поки є next:

all_items = []
page = 1
made = 0                      # лічильник запитів

while page is not None:
    if made >= 50:            # стеля: далі щось явно не так
        print("забагато сторінок, зупиняюсь")
        break

    r = requests.get(f"{BASE}/menu", params={"page": page}, timeout=5)
    r.raise_for_status()
    made += 1

    data = r.json()
    all_items.extend(data["items"])
    page = data["next"]       # None на останній сторінці

print("зібрано позицій:", len(all_items))
Стеля кроків обовʼязкова. Цикл довіряє чужому серверу: він крутиться рівно доти, доки той віддає next. Помилка на боці сервісу — і next починає вказувати сам на себе. Без лічильника твоя програма надсилатиме запити нескінченно, а ти дізнаєшся про це, коли сервіс тебе заблокує. Правило просте: будь-який цикл, умову виходу з якого визначає чужа програма, має власну стелю.

ВвічливістьЛіміти й 429

Чужий сервер — не твій. Він обслуговує ще тисячу клієнтів, і його час коштує грошей. Тому майже кожен API має ліміт частоти (rate limit): скільки запитів на хвилину або на годину дозволено одному клієнту. Перевищив — отримуєш код 429 Too Many Requests.

r = requests.get(f"{BASE}/limited", timeout=5)

print(r.status_code)               # 429
print(r.headers["Retry-After"])    # 2

Разом із 429 сервер зазвичай надсилає заголовок Retry-After. Це не порада, а пряма вказівка: скільки секунд чекати, перш ніж пробувати знову. Число в ньому дає сам сервер — він краще за тебе знає, коли твій ліміт відновиться. Ігнорувати його — найшвидший спосіб перетворити тимчасове обмеження на постійне блокування.

Ліміт 60 запитів на хвилину

Кожна крапка — один запит на осі однієї хвилини. Зелена пройшла, рожева відбита з кодом 429.

пройшло—
відбито 429—
Що читати: квапливий клієнт не встигає більше — він просто отримує відмови й даремно навантажує сервер. Ввічливий за ту саму хвилину доносить рівно стільки ж корисних запитів, скільки дозволено, і жодного разу не потрапляє під 429. Пауза не сповільнює роботу: сповільнює її ліміт, а пауза лише робить це без сварки.

Найпростіша ввічливість — time.sleep() між запитами в циклі. Якщо ліміт 60 запитів на хвилину, пауза в одну секунду тримає тебе рівно на межі й не вимагає жодної арифметики:

import time

for page in range(1, 6):
    r = requests.get(f"{BASE}/menu", params={"page": page}, timeout=5)
    print(page, r.status_code)
    time.sleep(1.0)          # не частіше ніж раз на секунду

НаполегливістьПовтори з відступом

Код 5xx означає «зараз не можу». Слово «зараз» тут ключове: сервіс міг перезапускатися, а через секунду вже працює. Тому на 5xx запит повторюють. Наш навчальний сервер має маршрут, який поводиться саме так: перші дві спроби — 503, третя — 200.

Питання не в тому, повторювати чи ні, а з якими паузами. І ось тут інтуїція підводить: здається, що чекати треба однаково, наприклад по секунді. А правильна відповідь — пауза має зростати: 1 секунда, 2, 4, 8. Це зветься експоненційний відступ (exponential backoff), бо кожна наступна пауза вдвічі довша за попередню.

Причина в тому, що 5xx найчастіше означає перевантаження. Якщо тисяча клієнтів отримали відмову й усі почали грюкати раз на секунду, сервер отримує ту саму тисячу запитів щосекунди — і не встає ніколи. Зростаюча пауза розводить клієнтів у часі: навантаження спадає само, і сервер отримує шанс піднятися. А ще вона дозволяє перечекати довгий збій за розумну кількість спроб: пʼять спроб із подвоєнням перечікують 1 + 2 + 4 + 8 = пʼятнадцять секунд, а пʼять спроб по секунді — лише чотири.

Три стратегії проти одного збою

Сіра зона — час, поки сервер лежить. Кружечок на осі — спроба: рожева отримала 503, зелена дочекалась 200. Спроб тут шість, а зростаюча пауза стартує з півсекунди, тож остання спроба припадає на 15.5 с.

спроб до успіху—
час до успіху—
Що читати: на короткому збої однакова пауза навіть трохи швидша — і це чесно. Але посунь повзунок за пʼять секунд: у неї закінчуються спроби, а зростаюча ще дістає. Зверни увагу й на кількість запитів у перші пʼять секунд: однакова пауза встигає надіслати шість, зростаюча — чотири. Різниця в тому, скільки ти додаєш до чужого перевантаження.

Кодом це кілька рядків. Пауза стартує маленькою й подвоюється:

import time

def get_with_retry(url, attempts=5, pause=1.0):
    """Повторює запит, поки сервер віддає 5xx. Пауза щоразу подвоюється."""
    for number in range(1, attempts + 1):
        r = requests.get(url, timeout=5)

        if r.status_code < 500:
            return r          # 2xx або 4xx — повторювати нічого

        print(f"спроба {number}: код {r.status_code}, чекаю {pause:.1f} с")
        time.sleep(pause)
        pause = pause * 2     # ось він, відступ

    return r                  # спроби вичерпані, віддаємо останню відповідь

Зверни увагу на умову r.status_code < 500. Повторюємо тільки пʼятірки. На 404 чи 422 повтор — це марна витрата часу й чужих ресурсів: відповідь не зміниться.

Повторювати можна не всі методи. GET, PUT і DELETE ідемпотентні — повторюй скільки треба. А от POST створює новий запис щоразу. Уяви найгірший випадок: сервер прийняв твій POST, створив замовлення — і зʼєднання обірвалося дорогою назад. Ти не отримав відповіді й повторюєш. Тепер замовлень два, і клієнта списали двічі. Тому POST або не повторюють узагалі, або повторюють із ключем ідемпотентності: власним випадковим рядком у заголовку, за яким сервер упізнає, що це той самий запит, і другого запису не створює.

ФормаВід форми відповіді до коду

Останній практичний навик — читати форму відповіді. JSON вкладений: словники всередині списків усередині словників. Дістати з такої структури потрібне число означає скласти шлях — ланцюжок звертань, де до словника ти йдеш по ключу, а до списку — за номером.

Клацни будь-який вузол дерева, і побачиш рядок, який треба написати в Python:

Дерево відповіді й шлях у Python

Клацни по будь-якому рядку дерева — унизу зʼявиться вираз, який дістає саме це значення.

type()—
значення—
Головне: квадратні дужки з рядком усередині — звертання до словника за ключем; квадратні дужки з числом — звертання до списку за номером. Обидва можуть впасти: KeyError, якщо ключа немає, IndexError, якщо список коротший. Тому на чужих даних безпечніше писати data.get("next") — воно поверне None замість помилки.

Найкорисніша звичка при знайомстві з новим API — надрукувати відповідь красиво, перш ніж писати шлях:

import json

print(json.dumps(r.json(), indent=2, ensure_ascii=False))

indent=2 розкладає структуру по рядках із відступами, а ensure_ascii=False лишає кирилицю кирилицею — обидва аргументи ми розбирали в темі 22. Дві секунди на цей рядок економлять пів години здогадок про те, як зветься потрібне поле.

ДовідникЯк читати документацію API

Кожен сервіс має власну документацію, і виглядають вони по-різному. Але шукати в них треба одне й те саме, і в такому порядку:

  1. Базовий URL. Спільний початок усіх адрес, на кшталт https://api.example.com/v2. Одна змінна в коді — і решта адрес складаються з нього.
  2. Автентифікація. Чи потрібен ключ, як його отримати й у якому заголовку передавати. Тут ховається сюрприз на кшталт «токен живе годину».
  3. Ліміти. Скільки запитів дозволено й за який період. Іноді ліміти різні для різних маршрутів, і найдорожчий тобі якраз і потрібен.
  4. Формат помилок. Які коди сервіс вживає й що кладе в тіло. Побачивши приклад заздалегідь, ти напишеш обробку відразу, а не після першого падіння.
  5. Пагінація. Як зветься параметр сторінки, який максимальний розмір, де сигнал кінця: поле next, заголовок Link чи просто порожній список.
  6. Версіонування. Номер версії у шляху (/v2/) або в заголовку. Він існує рівно для того, щоб сервіс міг змінити формат відповіді, не зламавши старих клієнтів.

І остання звичка: перш ніж писати код, зроби один запит вручну й подивись на відповідь. У документації трапляються застарілі приклади — а сервер завжди каже правду про себе.

ГлибшеДля тих, хто вже ходив в API

Кілька речей, які виринають далі й нечасто трапляються в перших туторіалах.

Retry-After буває датою. Стандарт дозволяє два формати: кількість секунд (2) і мітку часу за HTTP (Wed, 09 Sep 2026 08:00:00 GMT). Код, який робить int(r.headers["Retry-After"]), на другому форматі впаде з ValueError. Розбирати дату вміє email.utils.parsedate_to_datetime.

Пагінація буває не сторінками, а курсором. Замість ?page=2 сервер віддає непрозорий рядок і просить надіслати його наступного разу. Це не примха: доки ти йдеш сторінками, у колекцію можуть додати запис, позиції зсунуться, і одну ти побачиш двічі, а іншу пропустиш. Курсор указує на конкретне місце в даних і від вставок не залежить.

Умовні запити економлять трафік. У відповіді часто є заголовок ETag — короткий відбиток вмісту. Надішли його наступного разу в If-None-Match, і якщо нічого не змінилося, сервер відповість 304 Not Modified без тіла: той самий результат за майже нуль байтів.

Ліміт видно ще до відмови. Чимало сервісів повертають лічильники в заголовках: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Дивитись на Remaining і пригальмовувати заздалегідь набагато акуратніше, ніж дочекатись 429.

Не всі API — REST. Трапляється GraphQL, де адреса одна, а що саме повернути, описуєш ти сам у тілі запиту. Логіка з цієї теми — заголовки, коди, повтори, ліміти — лишається тією самою; міняється лише те, як формулюється питання.

ПідсумокЩо з цього забрати

Ми навчилися розмовляти з чужою програмою по мережі:

І остання думка, яка веде далі. Усе це працює за однієї умови: API існує — хтось у кавʼярні сів і зробив адресу, яка віддає меню в JSON. Але переважна більшість сторінок в інтернеті такої адреси не має: там лишається тільки верстка для людських очей, з якої ми починали. Що робити, коли дані видно на екрані, а взяти їх нема звідки, — тема наступної лекції про скрапінг. І перше, що там прозвучить: перевір, чи справді немає API, бо розбирати HTML — завжди гірший варіант.