У попередній темі ми дописали скрипт-звіт: програму, якій кажеш прямо в момент запуску, який файл читати й що з ним робити. Вона вийшла справді корисною — але в неї є мовчазне припущення. Файл із даними вже лежить на диску, і поклав його туди хтось. Руками. Сьогодні ми приберемо це припущення: навчимо програму йти по дані туди, де вони справді живуть — на чужий сервер.
Наскрізний приклад теми — та сама кавʼярня «Кома», що й у темі
22: позицій у меню тут чотири — рівно стільки, скільки треба цій темі, бо наскрізним є заклад, а не довжина списку. Тоді меню лежало у файлі
menu.csv, який ми самі й записали. Тепер уяви, що меню живе на сервері
кавʼярні, змінюється щоранку, і твоя програма має щоразу брати свіже.
МотивДані, яких немає на диску
Файл на диску має одну прикру властивість: він застигає в момент, коли його записали. Прайс постачальника, курс валют, розклад електричок, показники датчика, меню кавʼярні — усе це змінюється без твоєї участі. Тримати такі дані у файлі можна рівно доти, доки хтось не забув його оновити.
Спосіб отримати свіже той самий, що й у людей: запитати в того, у кого воно є. Уся різниця в тому, що замість людини питає програма, а замість слів використовує суворо описаний порядок — протокол. Той, яким користується майже все, що ти бачиш у браузері, зветься HTTP (HyperText Transfer Protocol — протокол передавання гіпертексту).
Назва історична: сьогодні HTTP возить не лише гіпертекст, а будь-що — таблиці, картинки, відео, показники датчиків, платежі.
МодельХто кого питає
Уся модель HTTP тримається на трьох реченнях, і їх варто прочитати повільно.
- Розмову завжди починає клієнт. Твоя програма — клієнт. Вона формулює запит і надсилає його. Сервер сам ніколи не заговорить першим: він мовчить, доки його не спитали.
- Одне питання — одна відповідь. На кожен запит сервер віддає рівно одну відповідь, і на цьому обмін закінчено.
- Сервер не памʼятає попереднього разу. Кожен запит для нього самодостатній. Якщо серверу треба знати, хто ти, це доведеться сказати у самому запиті — щоразу заново.
Третій пункт дивує найчастіше. У браузері ж видно, що сайт «памʼятає», що ти увійшов. Памʼятає — але не тому, що між вами є щось постійне: браузер просто щоразу докладає до запиту той самий розпізнавальний рядок. Постійного «підключення до сайту» не існує.
Що таки існує — це зʼєднання: канал між двома програмами, який
відкривають, використовують і закривають. Щоб його відкрити, треба знати дві речі.
Перша — адреса машини: чотири числа через крапку на кшталт
127.0.0.1, за якими мережа знаходить потрібний компʼютер серед усіх інших.
Друга — порт: номер дверей на цій машині, за якими сидить потрібна
програма. Одна машина тримає десятки програм одночасно, і без номера дверей незрозуміло,
до кого саме ми прийшли. Для HTTP типовий порт 80, для захищеного HTTPS — 443. У наших
прикладах сервер сидітиме на 127.0.0.1 — це особлива адреса, яка на будь-якій
машині означає «цей самий компʼютер», — з портом, який видасть система.
Але в коді ми пишемо не числа, а звичну адресу сторінки. Вона й містить усе потрібне, просто в іншому вигляді:
http://cafe.local/menu ↑ ↑ ↑ схема хост шлях
Схема каже, за яким протоколом розмовляти: http або
захищений https. Хост — імʼя машини; саме воно поїде в
заголовку Host, який ти побачиш у запиті через розділ. Імʼя — це ще не
адреса: перш ніж відкривати зʼєднання, програма питає в служби імен (DNS), яке число
ховається за cafe.local, і йде вже до числа. Шлях — те, що
саме ми в цієї машини просимо. А порту в цій адресі немає, і це звичайна річ: коли його
не написали, береться типовий для схеми — 80 для http, 443 для
https. Написати його можна явно, через двокрапку після хоста:
http://127.0.0.1:8000/menu — саме так виглядатимуть адреси в наших
прикладах.
Слово «зʼєднання» ще знадобиться в розділі про Session: там усе крутиться
навколо того, скільки разів ми його відкриваємо. А поки що подивись, з яких кроків
складається один обмін і що саме летить на кожному з них.
Один обмін по кроках
Натискай «крок» і дивись, що саме зараз передається. Праворуч — сирий текст, який іде дротом.
на дроті
БудоваАнатомія запиту й відповіді
І запит, і відповідь — це звичайний текст із дуже простою будовою. Спершу один особливий перший рядок, потім заголовки по одному на рядок, потім порожній рядок, потім (якщо є) тіло.
Запит
GET /menu HTTP/1.1 ← метод, шлях, версія протоколу Host: cafe.local ← до якого сайту звертаємось User-Agent: python-requests/2.34.2 Accept: */* Accept-Encoding: gzip Connection: keep-alive ← порожній рядок: заголовки скінчились
Перший рядок несе три речі. Метод — що саме ми хочемо зробити. Шлях — з чим саме. Версія — за якими правилами розмовляємо.
Методів кілька, і різниця між ними не технічна, а смислова: це домовленість про наміри, якої дотримуються всі. Головне питання до методу — чи змінює він щось на сервері і чи безпечно повторити його двічі.
| метод | намір | змінює дані | безпечно повторити |
|---|---|---|---|
| GET | дай мені це | ні | так |
| HEAD | дай лише заголовки, без тіла | ні | так |
| POST | ось нові дані, прийми | так | ні |
| PUT | хай за цією адресою буде ось це | так | так |
| PATCH | зміни ось цю частину | так | як домовиться сервіс |
| DELETE | прибери це | так | так |
Стовпчик «безпечно повторити» — не дрібниця, а те, від чого залежить, чи можна
автоматично перезапустити невдалий запит. Повторити GET можна скільки
завгодно: він нічого не міняє. Повторити POST — означає ризикнути другим
замовленням і другим списанням грошей. Властивість «повтор дає той самий результат, що
й один виклик» зветься ідемпотентністю (idempotence). PUT
ідемпотентний, хоч і змінює дані: «хай тут буде ось це» вдруге не змінює нічого нового.
PATCH стоїть окремо, і саме тому в таблиці не «так» і не «ні». Стандарт
його ідемпотентності не гарантує, а на ділі все вирішує тіло запиту: «постав ціну 50»
вдруге нічого не змінить, а «додай до ціни 5» щоразу робитиме дорожче. Отже, за
PATCH відповідь дає документація сервісу, а не сам метод.
Відповідь
HTTP/1.1 200 OK ← версія, код стану, пояснення словами Content-Type: text/plain; charset=utf-8 Content-Length: 148 Server: BaseHTTP/0.6 Python/3.12.2 ← порожній рядок Еспресо — 25 грн (кава) ← тіло Капучино — 45 грн (кава) Латте — 50 грн (кава) Чай — 30 грн (чай)
Будова дзеркальна. Замість методу й шляху — код стану (число, за яким рішення ухвалює програма) і пояснення (слова, які потрібні лише людині; жодна програма на них не дивиться). Далі заголовки, порожній рядок і тіло.
Оце й увесь HTTP на рівні, який треба розуміти. Руками такий текст ми не писатимемо жодного разу — його складе бібліотека. Але коли щось піде не так, дивитись треба саме сюди.
ІнструментПерший requests.get
У стандартній бібліотеці Python для HTTP є модуль urllib.request.
Він працює, нічого не треба ставити — і майже ніхто ним не користується напряму. Причина
проста: він написаний під іншу епоху, і найзвичайніші речі в ньому виходять довгими.
Тому мовою реального Python-коду став сторонній пакет requests.
Ставиться він так, як ми вчилися в темі 18:
python3 -m pip install requests
А далі один рядок:
код
import requests
r = requests.get(
"http://127.0.0.1:8000/menu",
timeout=5,
)
print(r.status_code)
print(r.reason)
print(r.headers["Content-Type"])
print(r.text)що надрукується
200 OK text/plain; charset=utf-8 Еспресо — 25 грн (кава) Капучино — 45 грн (кава) Латте — 50 грн (кава) Чай — 30 грн (чай)
Виклик requests.get() повертає обʼєкт Response.
Коли він у тебе в руках, обмін уже завершено: бібліотека відкрила зʼєднання, надіслала
запит, дочекалась відповіді й дочитала тіло до кінця. Далі це просто дані в памʼяті.
Найкорисніші його частини:
| звертання | що дає |
|---|---|
| r.status_code | код стану числом: 200, 404, 500 |
| r.reason | те саме словами: OK, Not Found |
| r.ok | True, якщо код менший за 400 |
| r.text | тіло як рядок |
| r.content | тіло як байти |
| r.headers | заголовки відповіді, як словник |
| r.url | адреса, за якою відповідь отримано насправді |
| r.history | ланцюг перенаправлень, якщо він був |
Аргумент timeout=5 у прикладі не для краси — про нього окремий розділ
нижче. Запамʼятай поки що просто як звичку: timeout ставиться завжди.
ВідповідьКоди станів як система
Код стану — трицифрове число, і вчити його напамʼять не треба, бо це не список, а система. Значення несе перша цифра; дві інші лише уточнюють.
| клас | сенс одним реченням | хто винен |
|---|---|---|
| 1xx | прийняв, продовжуй — службове, в коді майже не трапляється | — |
| 2xx | зробив те, що просив | — |
| 3xx | того, що просив, тут немає — воно за іншою адресою | — |
| 4xx | запит поганий | клієнт, тобто ти |
| 5xx | запит нормальний, зламався я | сервер |
Різниця між 4xx і 5xx — головне, що варто винести з цього
розділу. Вона відповідає на найпрактичніше питання: чи має сенс повторити запит.
На 4xx повтор дасть те саме, бо міняти треба запит. На 5xx
повтор може врятувати: сервер міг перевантажитись на секунду.
Покрути повзунок і подивись, як змінюється реакція програми. Коди між відомими теж щось означають: перша цифра працює навіть для номера, якого ніхто не бачив.
Що робити з цим кодом
Повзунок веде по всьому діапазону. Дивись не на назву, а на клас — саме він визначає дію.
Обидва інструменти для перевірки вже є в Response:
r.ok— простоTrueдля всього, що менше за 400. Зручно дляif. Зверни увагу:301теж даєTrue;r.raise_for_status()— мовчить на кодах до 400 і піднімаєrequests.exceptions.HTTPErrorна4xxі5xx.
перевірка числом
r = requests.get(url, timeout=5)
if r.status_code == 404:
print("такого меню немає")
elif r.ok:
print(r.text)
else:
print("сервер відповів",
r.status_code)перевірка винятком
r = requests.get(url, timeout=5)
try:
r.raise_for_status()
except requests.exceptions.HTTPError:
print("не 2xx:", r.status_code)
else:
print(r.text)Обирай за тим, чи вміє це місце коду щось зробити з поганим кодом. Якщо вміє —
перевіряй числом і розвʼязуй на місці. Якщо не вміє й задача просто зупинитись —
raise_for_status() коротший і чесніший, бо кидає виняток з описом
одразу там, де проблема виникла. Винятки ми розбирали
в темі 19.
БайтиЗаголовки, Content-Type і кракозябра
Заголовки — це пари «назва: значення», якими сторони повідомляють службові речі про
свій запит або свою відповідь. Найпростіший образ тут — посилка: тіло
відповіді це вміст коробки, а заголовки — наліпка зверху. На наліпці написано, що
всередині, скільки воно важить, хто відправник і чи можна коробку відкривати повторно.
Розділені вони не випадково: наліпку читають, не розкриваючи коробки, — саме тому
одержувач може вирішити, що робити з посилкою, ще до того, як дістане перший байт
вмісту. У requests вони доступні як
r.headers і поводяться майже як звичайний словник
(тема 09), тільки регістр назви не має
значення: r.headers["content-type"] і r.headers["Content-Type"] —
те саме.
Найважливіший заголовок відповіді — Content-Type. Він
відповідає на два питання одразу: що це за дані і в якому вони кодуванні.
Content-Type: text/plain; charset=utf-8 └ тип/підтип ┘ └ кодування ┘
Тепер найтонше місце теми. Мережею їдуть байти — і більше нічого.
Байт сам собою не знає, чи він частина літери. Щоб зібрати з байтів текст, треба знати
правило складання, тобто кодування. Звідси три різні речі в Response,
які часто плутають:
r.content— байти, рівно те, що приїхало. Тут ніколи нічого не псується;r.encoding— кодування, якимrequestsзбирається користуватись;r.text— рядок, отриманий ізr.contentза правиломr.encoding. Він не зберігається, а рахується щоразу заново.
Отже, кракозябра — це завжди не «зіпсовані дані», а правильні байти, зібрані за
неправильним правилом. І найчастіша причина ось яка: якщо сервер віддав щось
текстове, але charset не назвав, старий стандарт HTTP каже вважати таке
кодуванням ISO-8859-1 — західноєвропейським, де українських літер немає
взагалі.
сервер не назвав charset
# Content-Type: text/plain r.encoding 'ISO-8859-1' r.text[:14] 'Ð\x95Ñ\x81пÑ\x80еÑ\x81о' r.content[:6] b'\xd0\x95\xd1\x81\xd0\xbf'
два способи полагодити
# 1. розкодувати самому r.content[:14].decode("utf-8") 'Еспресо' # 2. підказати requests r.encoding = "utf-8" r.text[:7] 'Еспресо'
Зверни увагу на самі числа в зрізах: 14 байтів і 7 символів —
це те саме слово «Еспресо». Кожна українська літера в UTF-8 займає два байти, тож у байтах
слово вдвічі «довше», ніж у літерах. Тому зріз по r.content і зріз по
r.text ніколи не збігаються довжиною — вони міряють різні одиниці.
r.text, бери
r.content.decode(...). Здогад бібліотеки залежить від заголовків, які
пише чужий сервер, і може змінитись без попередження. Байти не змінюються ніколи.ПараметриЯк передати серверу слово
Досі ми просили сторінку цілком. Але частіше треба попросити щось конкретне: не «меню», а «меню, де є слово кава». Такі уточнення їдуть у самій адресі, після знака питання:
http://cafe.local/search?q=кава&limit=10 └ рядок запиту: пари ключ=значення через &
Спокуса склеїти таку адресу рядком дуже сильна: url + "?q=" + word — і
готово. Не роби так. Адреса — це не вільний текст: у ній є символи зі службовим
значенням, і твоє слово може випадково виявитись одним із них.
| символ | що означає в адресі | що буде при склеюванні |
|---|---|---|
| пробіл | нічого — його там просто не можна | рядок обірветься або дійде покаліченим |
| & | кінець параметра, далі наступний | значення обріжеться на амперсанді |
| = | межа між ключем і значенням | зайва межа посеред слова |
| # | початок якоря, на сервер не їде | усе після нього мовчки зникне |
| кирилиця | в адресі дозволені лише латиниця й цифри | залежить від того, хто по дорозі як здогадається |
Правильний спосіб — віддати requests словник і не думати:
r = requests.get(url, params={"q": "чай & кава"}, timeout=5) print(r.url) http://cafe.local/search?q=%D1%87%D0%B0%D0%B9+%26+%D0%BA%D0%B0%D0%B2%D0%B0
Те, що вийшло, зветься відсотковим кодуванням (percent-encoding).
Правило його просте: слово перетворюють на байти в UTF-8, і кожен байт, який не є
латинською літерою, цифрою чи одним із кількох безпечних знаків, записують як
% і два шістнадцяткові знаки. Українська літера «ч» у UTF-8 — це два байти,
тому вона й перетворюється на %D1%87. А пробіл у рядку запиту традиційно
пишуть як +.
Поле нижче приймає будь-яке значення. Спробуй пробіл, амперсанд, кирилицю, символ
відсотка — і подивись, що з них зробить params= і що вийде, якщо склеїти
рядок самотужки.
Конструктор адреси
Угорі — те, що збере params=. Унизу — те, що вийде зі склеєного рядка, і що з нього прочитає сервер.
ТерпінняТаймаут
Ось найкоротший спосіб зробити програму, яка одного дня зависне назавжди:
r = requests.get(url) ← без timeout
Мережа має властивість, якої немає в диска: вона може не відповісти й не відмовити. Сервер прийняв зʼєднання й замовк; кабель висмикнули посеред відповіді; проміжний пристрій мовчки проковтнув пакет. У всіх цих випадках твоя програма чекає — і без явної межі чекатиме доти, доки не втрутиться операційна система, а це можуть бути десятки хвилин.
timeout ставить цю межу:
requests.get(url, timeout=5) ← 5 секунд на кожен етап requests.get(url, timeout=(3, 10)) ← 3 на зʼєднання, 10 на читання
Важлива тонкість, через яку люди ставлять таймаут не туди: це не
обмеження на весь запит. Це межа тиші — скільки чекати наступної порції даних.
Сервер, який щосекунди надсилає по байту, з timeout=5 не обірветься ніколи.
Для повного обмеження часу потрібні інші засоби; для переважної більшості задач
достатньо таймауту тиші.
Коли межу перевищено, летить requests.exceptions.Timeout. У нього є два
підвиди — ConnectTimeout (не вдалось навіть зʼєднатись) і
ReadTimeout (зʼєднались, але відповіді не дочекались); ловити достатньо
загальний Timeout.
Встиг чи не встиг
Ліворуч — скільки сервер думає. Праворуч — скільки ми готові чекати. Вердикт рахується з цих двох чисел.
Дві бідиПомилка мережі проти помилки HTTP
Тепер зведімо докупи найважливішу відмінність теми. Є два зовсім різні сорти неприємностей, і плутати їх дорого.
- Відповіді немає. Сервер не знайдено, зʼєднання відкинуто, кабель
мовчить. Летить виняток:
ConnectionErrorабоTimeout. Коду стану не існує — його ніхто не надсилав; - Відповідь є, і вона погана. Сервер працює, він тебе почув і
відповів
404чи500. Винятку немає: з погляду Python усе відпрацювало.
Звідси й різна реакція. На 500 ти можеш подивитись у відповідь, записати
її в журнал, показати користувачеві текст помилки — відповідь у тебе в руках. На
ConnectionError у тебе немає нічого, крім факту невдачі.
Усі винятки requests — нащадки одного:
requests.exceptions.RequestException ├── ConnectionError не вдалося дістатись сервера │ └── ConnectTimeout (він же й Timeout) ├── Timeout │ └── ReadTimeout зʼєднались, відповіді не дочекались ├── TooManyRedirects ланцюг перенаправлень задовгий └── HTTPError його піднімає raise_for_status()
Тому except requests.exceptions.RequestException ловить усе, що може
піти не так у запиті, — зручно для верхнього рівня програми. Але для рішення «повторити
чи здатись» потрібен точніший розбір:
Де саме зламалось
Перемикай причину й дивись, до якої точки дійшов запит, що отримає програма і чи допоможе повтор.
Timeout і
ConnectionError. І тільки для методів, які безпечно повторити:
другий GET нікому не зашкодить, а другий POST може
оформити друге замовлення.ДорогаПеренаправлення
Клас 3xx означає: «того, що ти просиш, за цією адресою немає, але я знаю,
де воно». Разом із таким кодом сервер надсилає заголовок Location — нову
адресу.
| код | сенс | чи змінює метод |
|---|---|---|
| 301 | переїхали назавжди | так, POST може стати GET |
| 302 | тимчасово тут | так, історично |
| 303 | подивись отам, і саме через GET | так, навмисно |
| 307 | тимчасово, метод не чіпай | ні |
| 308 | назавжди, метод не чіпай | ні |
requests за замовчуванням іде за перенаправленням сам, і робить це доти,
доки не дійде до відповіді без Location. Тому в звичайному коді ти
301 навіть не побачиш — одразу отримаєш кінцеву сторінку. Слід при цьому
не зникає:
r.url— адреса, за якою відповідь отримано насправді, а не та, яку ти просив;r.history— список проміжних відповідей, у порядку проходження. Порожній, якщо перенаправлень не було;allow_redirects=False— не ходити нікуди; тоді ти отримаєш саме301і зможеш прочитатиLocationсам.
Ланцюг перенаправлень
Постав довжину ланцюга й перемкни allow_redirects. Праворуч — що після цього лежить у відповіді.
requests частину з них при зміні адреси навмисно не переносить —
з міркувань безпеки. Тому запит, який «працює в браузері», з бібліотеки може
несподівано отримати 401. Лікується тим, що адресу одразу пишуть із
https.ОщадністьSession: одне зʼєднання замість сорока
Повернімось до зʼєднання. Кожен виклик requests.get() — це окрема
маленька історія: відкрити зʼєднання, надіслати запит, отримати відповідь, закрити
зʼєднання. Якщо запитів сорок, ця історія повторюється сорок разів.
Відкрити зʼєднання не безкоштовно. Спершу TCP-рукостискання — три пакети туди-назад.
Якщо адреса на https, поверх додається рукостискання TLS: ще один-два
обміни, плюс перевірка сертифіката. На сервері за океаном кожен такий обмін коштує
сотню мілісекунд до того, як полетів твій перший байт.
Session ламає цю схему: вона тримає відкриті зʼєднання
й видає їх наступним запитам до того самого сервера.
сорок історій
for name in names:
r = requests.get(
url, params={"q": name},
timeout=5)
print(r.status_code)
# 40 запитів = 40 зʼєднаньодна історія
with requests.Session() as s:
s.headers.update(
{"X-Client": "menu-bot"})
for name in names:
r = s.get(url,
params={"q": name},
timeout=5)
print(r.status_code)
# 40 запитів = 1 зʼєднанняТепер чесно про виграш — бо тут підручники часто обіцяють не те, що можна побачити.
Я поміряв: сорок запитів до локального сервера, медіана з трьох прогонів. Виграш у часі виявився цілком залежним від однієї-єдиної налаштування сервера:
| сервер | із Session | без Session | різниця |
|---|---|---|---|
| як є | 1758 мс | 214 мс | Session у 8 разів гірша |
| з вимкненим Нейглом | 81 мс | 204 мс | Session у 2.5 раза краща |
Перший рядок виглядає як спростування всього розділу — і саме тому його варто зрозуміти. Число в ньому нестійке: воно залежить від таймера відкладеного підтвердження, тож на іншій машині перший рядок легко дасть і вдвічі менше, і вдвічі більше. Стійке тут інше — напрямок: із Нейглом постійне зʼєднання програє, без нього виграє приблизно втричі. Саме напрямок і варто запамʼятати, а не множник. Виною давня оптимізація TCP на імʼя алгоритм Нейгла: щоб не слати мережею крихітні пакети, вона притримує дрібні порції, доки не підтвердять попередні. У парі з іншою оптимізацією — відкладеним підтвердженням на боці одержувача — виходить взаємна очікувальна пауза близько сорока мілісекунд. На постійному зʼєднанні вона припадає на кожну відповідь, і сорок запитів набирають ті самі 1.7 секунди. На короткоживучих зʼєднаннях паузи немає, бо закриття зʼєднання проштовхує дані одразу.
Мораль подвійна. Перша: числа треба міряти, а не переказувати. Друга, важливіша для нас: час — поганий доказ у цій темі. Він залежить від налаштувань обох сторін, від навантаження машини й від фази місяця.
Надійний доказ інший — кількість зʼєднань. Вона детермінована: сорок
запитів через Session відкривають одне зʼєднання, без неї —
сорок. Це число однакове на будь-якій машині, і саме воно пояснює,
навіщо Session існує.
Скільки зʼєднань відкриється
Угорі — з Session, унизу — без неї. Кожен прямокутник — окреме відкрите зʼєднання.
Зʼєднання — не єдина користь. Session також тримає спільні заголовки
(задав User-Agent раз — їде з кожним запитом) і cookies, отримані від
сервера. Тому для будь-якої серії запитів до одного сервера Session —
типовий вибір, а поодинокий requests.get() лишається для разових звернень.
requests.get() насправді
теж створює Session — на один запит, і одразу її закриває. Тобто різниця
не в тому, є сесія чи ні, а в тому, скільки вона живе.ГлибшеДля тих, хто вже ходив у мережу
Кілька речей, які не потрібні для першого запиту, але рано чи пізно зустрінуться.
Стиснення відбувається непомітно. requests сам додає
Accept-Encoding: gzip, і сервер зазвичай надсилає тіло стисненим. Тому
len(r.content) може бути значно більшим за Content-Length:
заголовок описує те, що летіло дротом, а r.content — уже розтиснуте.
Це не помилка й не привід порівнювати одне з одним.
Тіло за замовчуванням цілком опиняється в памʼяті. Для меню з чотирьох
рядків це байдуже, для файлу на два гігабайти — ні. Тоді потрібен
stream=True і читання шматками через r.iter_content(...);
файл при цьому пишеться на диск, минаючи памʼять.
Твоя програма представляється. Заголовок User-Agent за
замовчуванням — python-requests/2.34.2. Деякі сервери за цим рядком
відповідають інакше або відмовляють. Ставити чуже імʼя, вдаючи браузер, — погана ідея;
натомість добрий тон — назватись чесно, вказавши назву свого інструмента й спосіб
звʼязку.
Повторів «із коробки» немає. requests навмисно не
перезапускає невдалі запити сам — бо не може знати, чи безпечно повторити саме твій.
Найпростіший робочий повтор — цикл із наростаючою паузою (одна секунда, дві, чотири) і
з обовʼязковою перевіркою, що метод повторювати можна. Якщо сервер відповів
429 і додав заголовок Retry-After, це прямо сказаний час, який
треба почекати, — не вигадуй свій.
Кодування іноді вгадують. Коли charset не названо, а
requests не має правила, він може звернутись до r.apparent_encoding —
здогаду за статистикою байтів. Здогад буває правильним, але це залишається здогадом:
на коротких відповідях він помиляється найчастіше.
HTTP/1.1 не єдиний. Сучасні сервери говорять і HTTP/2, де кілька
запитів їдуть одним зʼєднанням паралельно. requests цього не вміє, і для
переважної більшості задач це не проблема. Якщо колись знадобиться — сусідня бібліотека
httpx має майже такий самий інтерфейс і вміє HTTP/2 та асинхронність
(тема 37).
ПідсумокЩо ми тепер уміємо
Наша програма більше не залежить від того, чи поклав хтось файл у потрібну теку:
- модель — клієнт питає, сервер відповідає; одне питання, одна відповідь, жодної памʼяті про попередній раз;
- будова — рядок із методом і шляхом, заголовки, порожній рядок, тіло; у відповіді замість методу код стану;
requests.get— іResponse, у якому вже все є:status_code,headers,text,content,url,history;- коди станів — читай першу цифру:
4xxвинен ти,5xxвинен сервер;404не кидає винятку, для цього єraise_for_status(); - байти проти тексту —
r.contentне бреше ніколи,r.textзалежить відr.encoding, а кракозябра — це неправильно зібрані правильні байти; params=— словник замість склеювання рядків; відсоткове кодування зробить бібліотека, аr.urlпокаже факт;timeout— не забувай ніколи, це межа тиші;- дві біди — виняток означає, що відповіді немає взагалі; поганий код означає, що відповідь є і її можна прочитати;
Session— одне зʼєднання замість сорока, спільні заголовки й cookies.
І тут одразу видно наступну межу. Подивись, що ми приносимо: r.text —
рядок. Для меню з чотирьох напоїв його ще можна розібрати руками, порізавши на рядки.
Але сервери відповідають програмам не текстом для очей, а даними для програм:
той самий наш сервер має адресу /menu.json, за якою лежить те саме меню —
але у форматі, який Python перетворює на список словників одним рухом. А ще там будуть
ключі доступу, ліміти на кількість запитів і домовленості про те, як просити сторінками.
Це робота з API — наступна тема.