HTTP заявки с requests
#
HTTP#
Hyper-text Transfer Protocol е протокол за трансфер на информация (в приложния слой на OSI модела), който е стандарт за комуникация в мрежата.
Версии:
HTTP/1 - от 1996г.
HTTP/2 - от 2015г.
HTTP/3 - от 2022г.
Примерна HTTP заявка:
GET / HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Винаги първия ред е във формат {HTTP метода} {URI} HTTP/{версия}
. В случая искаме да вземем ресурсът, намиращ се на /
(т.е. root-a на example.com) с GET метода.
След него на всеки ред стои 1 хедър (заглавие) във формат {име}: {стойност}
Примерен HTTP отговор на заявка:
HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 155
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
ETag: "3f80f-1b6-3e1cb03b"
Accept-Ranges: bytes
Connection: close
<html>
<head>
<title>An Example Page</title>
</head>
<body>
<p>Hello World, this is a very simple HTML document.</p>
</body>
</html>
Пърият ред винаги е във формат HTTP/{версия} {статус код} {има на статус кода}
. В случая той е “200 OK”. (лист с всички кодове: тук)
След това са хедърите, след които има празен ред и започва тялото на отговора. В случая това е HTML страница, “намираща” се на example.com.
Методи#
GET (взимане на ресурс) (заявката може да няма тяло)
POST (изпращане на ресурс) (заявката има тяло)
PUT (заменяне на ресурс) (заявката има тяло)
PATCH (промяна на част от ресурс) (заявката има тяло)
DELETE (изтриване на ресурс) (заявката може да няма тяло)
HEAD (взимане само на хедърите на ресурс (като GET, но без върнато тяло))
OPTIONS (взимане на методите, поддържани от ресурса) (заявката може да няма тяло)
Статус кодове#
200 - 299: Успешно изпълнена заявка
300 - 399: Пренасочване
400 - 499: Грешка на клиента
500 - 599: Грешка на сървъра
Добавяне на requests
#
Библиотеката requests
не е вградена в езика (repo: psf/requests), затова трябва да се инсталира допълнително.
Чрез package manager-a pip
това става с командата:
pip install requests # sometimes pip3 is the right one though
!pip install requests
Collecting requests
Downloading requests-2.28.1-py3-none-any.whl (62 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.8/62.8 kB 1.2 MB/s eta 0:00:00[36m0:00:01
?25hCollecting idna<4,>=2.5
Downloading idna-3.4-py3-none-any.whl (61 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.5/61.5 kB 1.6 MB/s eta 0:00:00
?25hCollecting urllib3<1.27,>=1.21.1
Downloading urllib3-1.26.13-py2.py3-none-any.whl (140 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 140.6/140.6 kB 2.6 MB/s eta 0:00:00[31m4.5 MB/s eta 0:00:01
?25hCollecting certifi>=2017.4.17
Downloading certifi-2022.9.24-py3-none-any.whl (161 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 161.1/161.1 kB 3.0 MB/s eta 0:00:00 MB/s eta 0:00:01
?25hCollecting charset-normalizer<3,>=2
Downloading charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.13
След това трябва да се включи в текущия модул чрез import requests
:
import requests
Методи#
В модула има по една функция за всеки един HTTP метод. Всички те са с еднаква сигнатура, която по подразбиране изисква само един аргумент - URL адреса на ресурса.
requests.get("https://fmi.uni-sofia.bg")
<Response [200]>
requests.post("https://google.com")
<Response [405]>
requests.delete("https://tesla.com") # cancel Musk
<Response [501]>
Всеки един от тези методи връщат Response
обект, в който се съдържа целия отговор.
Проверка дали заявката е успешна#
Статус кода на отговора можем да вземем чрез status_code
атрибута (тип int
):
for url in [
"https://httpbin.org/status/204",
"https://httpbin.org/status/404",
]:
response = requests.get(url)
if response.status_code in range(200, 400):
print("Request went well. Status code = ", response.status_code)
else:
print("Sum Ting Went Wong. Status code = ", response.status_code)
Request went well. Status code = 204
Sum Ting Went Wong. Status code = 404
Горния начин на проверка обаче е често срещан - всички 2хх и 3хх статус кодове означават, че грешка на клиента или съвръра не е имало. Затова Response
обекта има предефиниран __bool__
, който оценява self
по същия начин като в горното сравнение:
for url in [
"https://httpbin.org/status/204",
"https://httpbin.org/status/404",
]:
response = requests.get(url)
if response:
print("Request went well. Status code = ", response.status_code)
else:
print("Sum Ting Went Wong. Status code = ", response.status_code)
Request went well. Status code = 204
Sum Ting Went Wong. Status code = 404
Ако пък искаме да работим с изключения, може да извикаме raise_for_status
, което би хвърлило HTTPError
ако не е успешна заявката:
from requests.exceptions import HTTPError
for url in [
"https://httpbin.org/status/204",
"https://httpbin.org/status/404",
"example.com", # thisis invalid so it will not even get to the `.raise_for_status()`
]:
try:
response = requests.get(url)
response.raise_for_status()
except HTTPError as http_err:
print(f'HTTP error occurred: {http_err}')
except Exception as err:
print(f'Other error occurred: {err}')
else:
print('Success!')
Success!
HTTP error occurred: 404 Client Error: NOT FOUND for url: https://httpbin.org/status/404
Other error occurred: Invalid URL 'example.com': No scheme supplied. Perhaps you meant http://example.com?
Взимане на хедърите и съдържанието от отговора#
response = requests.get("https://httpbin.org/json")
response
<Response [200]>
Заглавните части (хедърите) са в headers
, което връща обект, подобен на dict[str, str]
, но с case-insensitive ключове:
response.headers
{'Date': 'Mon, 28 Nov 2022 01:08:13 GMT', 'Content-Type': 'application/json', 'Content-Length': '429', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true'}
response.headers["CONTENT-TYPE"]
'application/json'
content
връща тялото като bytes
:
response.content
b'{\n "slideshow": {\n "author": "Yours Truly", \n "date": "date of publication", \n "slides": [\n {\n "title": "Wake up to WonderWidgets!", \n "type": "all"\n }, \n {\n "items": [\n "Why <em>WonderWidgets</em> are great", \n "Who <em>buys</em> WonderWidgets"\n ], \n "title": "Overview", \n "type": "all"\n }\n ], \n "title": "Sample Slide Show"\n }\n}\n'
Ако очакваме да е текстово съдържанието, можем да ползваме и text
, за да ги конвертираме в str
. По подразбиране encoding
-ът е “utf-8”, може да се променя от едноименния атрибут.
print(response.text) # print will just prettify the output
{
"slideshow": {
"author": "Yours Truly",
"date": "date of publication",
"slides": [
{
"title": "Wake up to WonderWidgets!",
"type": "all"
},
{
"items": [
"Why <em>WonderWidgets</em> are great",
"Who <em>buys</em> WonderWidgets"
],
"title": "Overview",
"type": "all"
}
],
"title": "Sample Slide Show"
}
}
JSON като формат за пренос на данни е най широко-използвания сред HTTP услугите поради леснотата на работата с него. В случая отговора е точно в такъв вид и можем да използваме и помощния метод json()
, който ни връща отговора като dict
(или list
, е зависимост от обекта, който седи като корен на JSON-a):
response.json()
{'slideshow': {'author': 'Yours Truly',
'date': 'date of publication',
'slides': [{'title': 'Wake up to WonderWidgets!', 'type': 'all'},
{'items': ['Why <em>WonderWidgets</em> are great',
'Who <em>buys</em> WonderWidgets'],
'title': 'Overview',
'type': 'all'}],
'title': 'Sample Slide Show'}}
Параметри на методите за заявката#
Можем да видим какво се съдържа в заявката с обекта върнат от .request
:
response.request
<PreparedRequest [GET]>
response.request.url
'https://httpbin.org/json'
response.request.headers
{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive'}
response.request.body
С params
задавaме query parameters - параметри на заявката. Те задължително трябва да бъдат подавани като dict[str, str]
(може още като list[tuple[str, str]]
или директно като bytes
) като стойност на именования аргумент params
:
requests.get(
"https://zamunda.net/catalogs/tv",
params={
"search": "Mr+Robot",
"field": "name",
"comb": "yes",
}
)
<Response [200]>
Note: В GET
заявките те биват слепени към края на URL-a след ?
във формат name1=value1&name2=value2&...&name=value
. За останалите HTTP методи обаче това не е в сила.
За тези, които поддържат добавянето на данни в заявката, това става чрез аргумента data
:
requests.post('https://httpbin.org/post', data={'key':'value'})
<Response [200]>
Ако искаме да изпратим допълнителни хедъри, задаваме стойност на аргумента headers
:
response = requests.get(
"https://api.github.com/search/repositories",
params={"q": "42+language:python"},
headers={"Accept": "application/vnd.github.v3.text-match+json"}
# ^ така казваме на Github сървъра, че искаме да ни върне и text-matches списък от обекти
# за всяко намерено репозитори
)
response.json()["items"][0]["text_matches"]
[{'object_url': 'https://api.github.com/repositories/10340514',
'object_type': 'Repository',
'property': 'description',
'fragment': 'Modules to convert numbers to words. 42 --> forty-two',
'matches': [{'text': '42', 'indices': [37, 39]}]}]
Ако не искаме автоматичното следване на редиректи, можем да го изключим с allow_redirects=False
:
url = "https://zamunda.net/catalogs/tv"
params={
"search": "Mr+Robot",
"field": "name",
"comb": "yes",
}
resp = requests.get(url, params=params)
print(resp.url)
resp = requests.get(url, params=params, allow_redirects=False)
print(resp.url)
https://zamunda.net/login.php?returnto=%2Fcatalogs%2Ftv%3Fsearch%3DMr%252BRobot%26field%3Dname%26comb%3Dyes
https://zamunda.net/catalogs/tv?search=Mr%2BRobot&field=name&comb=yes
Ако не ни трябва проверка за SSL сертификати, задаваме verify=False
:
requests.get('https://api.github.com', verify=False)
/opt/homebrew/lib/python3.10/site-packages/urllib3/connectionpool.py:1045: InsecureRequestWarning: Unverified HTTPS request is being made to host 'api.github.com'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/1.26.x/advanced-usage.html#ssl-warnings
warnings.warn(
<Response [200]>
По подразбиране методите могат да чакат за отговор неопределено време, така че винаги е хубаво да сложим и timeout
. При прехвърляне на зададеното максимално време се хвърля Timeout
изключение:
from requests.exceptions import Timeout
susi = "https://susi.uni-sofia.bg/ISSU/forms/students/ElectiveDisciplinesSubscribe.aspx"
try:
r = requests.get(susi, timeout=0.05) # в секунди; нарочно толкова малък, за да хвърлим грешка
except Timeout:
print("СУСИ ке падне")
else:
print("Абсурд, записах избираемите")
СУСИ ке падне
JSON#
Ако тялото на съобщението, което искаме да пратим е във вид на JSON, то вместо data
за удобство можем да ползваме json
аргумента, подавайки dict
:
response = requests.post('https://httpbin.org/post', json={'key':'value'})
response.json()['data']
'{"key": "value"}'
За работа с JSON обекти имаме вградената библиотека json
. С json.dumps
можем да превърнем JSON dict
стринг, а с json.loads
- обратното:
import json
fetched_json_str = """
{
"name": "亚历山大",
"age": 24.420,
"hobbies": [
"editing text files",
"sh*tposting music",
"drinking craft beer",
"creating existential crisises"
],
"sorrow": true,
"purpose": null
}
"""
json_dict = json.loads(fetched_json_str)
print("loads -> ,", json_dict)
print("dumps -> ", json.dumps(json_dict)) # back to a string
loads -> , {'name': '亚历山大', 'age': 24.42, 'hobbies': ['editing text files', 'sh*tposting music', 'drinking craft beer', 'creating existential crisises'], 'sorrow': True, 'purpose': None}
dumps -> {"name": "\u4e9a\u5386\u5c71\u5927", "age": 24.42, "hobbies": ["editing text files", "sh*tposting music", "drinking craft beer", "creating existential crisises"], "sorrow": true, "purpose": null}
С dump
и load
пък може да запишем/прочетем JSON директно от файл. Oсвен това, има начини за сериализация/десериализация на custom обекти чрез JSONEncoder
и JSONDecoder
. Повече информация тук.
Автентикация#
Общоприето е удостоверянето на самоличността да се случва най-често през хедъра Authentication
, пращан от клиентите. Стойността му е от вид “{тип на token} {token}”, като типовете могат да са Basic, Bearer и др. Със създаването му ни улеснява auth
параметъра:
r = requests.get("https://api.github.com/user", auth=("exampleuser", "examplepassword"))
r.request.headers["Authorization"]
'Basic ZXhhbXBsZXVzZXI6ZXhhbXBsZXBhc3N3b3Jk'
По подразбиране видът на удостоверяване е Basic, т.е. token-ът представлява просто base64-енкодираният стринг "{името}:{паролата}"
.
import base64
token = r.request.headers["Authorization"].split()[1]
decoded = base64.b64decode(token).decode("utf-8")
print(f"{token} is actually {decoded}")
ZXhhbXBsZXVzZXI6ZXhhbXBsZXBhc3N3b3Jk is actually exampleuser:examplepassword
Можем да ползваме и AuthBase
наследник, който да подадем като стойност на auth
. По подразбиране се използва HTTPBasicAuth
:
from requests.auth import HTTPBasicAuth
r = requests.get(
"https://api.github.com/user",
auth=HTTPBasicAuth("exampleuser", "examplepassword")
)
r.request.headers["Authorization"]
'Basic ZXhhbXBsZXVzZXI6ZXhhbXBsZXBhc3N3b3Jk'
Можем и собствени начини на автентикация да дефинираме чрез наследяване. Тогава трябва задължително в __call__
да променим дадения Request
така, че да има необходимите хедъри:
from requests import PreparedRequest
from requests.auth import AuthBase
class CustomAuth(AuthBase):
"""Implements a custom authentication scheme."""
def __init__(self, token: str):
self.token = token
def __call__(self, r: PreparedRequest) -> PreparedRequest:
"""Attach an API token to a custom auth header."""
r.headers['X-TokenAuth'] = f"{self.token}"
return r
r = requests.get('https://httpbin.org/get', auth=CustomAuth('deadbeef'))
r.request.headers
{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'X-TokenAuth': 'deadbeef'}
Сесии#
Потребителските сесии се използват за да се запазят настройки през различните заявки. Например ако искаме един и същ начин на оторизация да се използва за няколко заявки:
import requests
with requests.Session() as session:
session.auth = ("username", "password")
response = session.get('https://api.github.com/user')
print(response.request.headers)
response = session.patch('https://api.github.com/user')
print(response.request.headers)
{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Authorization': 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='}
{'User-Agent': 'python-requests/2.28.1', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '0', 'Authorization': 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='}
Адаптери#
За да добавим допълнителна функционалност/логика към всяка заявка за конкретна услуга, можем да използваме собствен TransportAdapter
, който да закачим към сесията. В request
идва вграден един такъв, наречен HttpAdapter
, с който можем например да дефинираме колко пъти да опитаме отново да изпратим заявката, в случай че е неуспешна:
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError
github_adapter = HTTPAdapter(max_retries=3)
with requests.Session() as session:
session.mount('https://api.github.com', github_adapter)
try:
session.get('https://api.github.com')
except ConnectionError as ce:
print(ce)
Неблокиращи решения#
Извикването на get
, post
и т.н. блокира изпълнението на програмата, докато content
не се свали. В случай, че се търси по-асинхронен подход, то съществуват билбиотеки като requests-threads
, grequests
, httpx
и др.