requests 2.34: HTTP for humans на практике
Библиотека requests — это de facto стандарт HTTP-клиента в Python с 2011 года. Кеннет Рейтц написал её с лозунгом “HTTP for humans” — в противовес стандартному urllib, который был мучительно громоздким. На пике, по статистике pypistats.org, requests скачивается 300+ миллионов раз в месяц — больше, чем любая другая Python-библиотека.
Для junior data engineer requests — это первый и самый важный инструмент. 80 процентов твоих ETL pipeline-ов будут на нём построены: дёрнуть API, получить JSON, разобрать. В этом уроке разберём установку версии 2.34, базовые методы, как правильно передавать параметры, как читать response, что такое encoding-проблемы и как их решать, и как работать с большими файлами через streaming.
Установка
$ pip install requests==2.34.0
Версия 2.34 (актуальная на 2026) поддерживает Python 3.8+ (но мы используем 3.13). Зависимости: charset-normalizer, idna, urllib3, certifi — обычно ставятся автоматически.
>>> import requests
>>> requests.__version__
'2.34.0'
Первый запрос
import requests
r = requests.get("https://jsonplaceholder.typicode.com/users/1")
print(r.status_code) # 200
print(r.json())
# {
# "id": 1,
# "name": "Leanne Graham",
# "email": "[email protected]",
# ...
# }
Это всё. Один импорт, одна строка, готовый JSON-объект на выходе. Сравни с urllib (стандартная библиотека):
# Эквивалент на urllib (ужас)
import urllib.request
import json
with urllib.request.urlopen("https://jsonplaceholder.typicode.com/users/1") as response:
data = json.loads(response.read().decode("utf-8"))
print(data)
Разница говорит сама за себя — отсюда и слоган “for humans”.
HTTP методы — все на месте
Каждый HTTP метод — отдельная функция верхнего уровня:
import requests
# GET -- чтение
requests.get("https://api.example.com/users/123")
# POST -- создание (или generic операция)
requests.post("https://api.example.com/users", json={"name": "Alice"})
# PUT -- полная замена
requests.put("https://api.example.com/users/123", json={"name": "Alice", "email": "..."})
# PATCH -- частичное обновление
requests.patch("https://api.example.com/users/123", json={"email": "[email protected]"})
# DELETE -- удаление
requests.delete("https://api.example.com/users/123")
# HEAD -- только заголовки
requests.head("https://api.example.com/users/123")
# OPTIONS -- какие методы поддерживает ресурс
requests.options("https://api.example.com/users/123")
Все возвращают объект Response. С ним работаем дальше.
Параметры запроса: params, data, json, headers, files
Это пять главных kwargs, которые ты будешь использовать постоянно.
params — query string
# Соберёт URL: https://api.github.com/search/repositories?q=python&sort=stars&per_page=10
r = requests.get(
"https://api.github.com/search/repositories",
params={
"q": "python",
"sort": "stars",
"per_page": 10,
}
)
print(r.url)
params — это dict (или list of tuples). requests сам делает URL-encoding пробелов, спецсимволов, не-ASCII.
# Пробел в значении
requests.get("https://example.com/search", params={"q": "data engineer"})
# URL: https://example.com/search?q=data+engineer
# Не-ASCII (русский)
requests.get("https://example.com/search", params={"q": "питон"})
# URL: https://example.com/search?q=%D0%BF%D0%B8%D1%82%D0%BE%D0%BD
json — отправить JSON в body
r = requests.post(
"https://api.example.com/users",
json={"name": "Alice", "email": "[email protected]"}
)
# requests автоматически:
# 1. Сериализует dict в JSON
# 2. Выставит Content-Type: application/json
# 3. Положит в body
Это почти всегда то, что тебе нужно при работе с REST API.
data — form-encoded body или raw
# Form-encoded (как HTML <form>)
requests.post("https://api.example.com/login", data={"username": "alice", "password": "..."})
# Content-Type: application/x-www-form-urlencoded
# body: username=alice&password=...
# Raw bytes
requests.post("https://api.example.com/upload", data=b"raw binary content")
# Raw string
requests.post("https://api.example.com/upload", data="text content")
headers — кастомные заголовки
r = requests.get(
"https://api.example.com/users",
headers={
"Authorization": "Bearer eyJhbGc...",
"User-Agent": "MyETL/1.0",
"Accept": "application/json",
"X-Request-Id": "abc-123",
}
)
Headers — обычный dict. requests добавит свои дефолтные (Accept-Encoding, User-Agent), которые можно переопределить.
files — multipart/form-data (загрузка файлов)
with open("report.csv", "rb") as f:
r = requests.post(
"https://api.example.com/upload",
files={"file": f} # 'file' -- имя поля формы
)
С метаданными:
files = {
"file": ("report.csv", open("report.csv", "rb"), "text/csv")
# (filename, fileobj, content_type)
}
r = requests.post("https://api.example.com/upload", files=files)
files= автоматически выставляет Content-Type: multipart/form-data; boundary=....
Если нужно отправить и файл, и обычные form-поля одновременно — передай files= и data= вместе. requests их объединит в один multipart запрос.
Response: что внутри
r = requests.get("https://jsonplaceholder.typicode.com/users/1")
# Статус
r.status_code # 200
r.ok # True (status < 400)
r.reason # 'OK'
# Тело
r.text # str (декодированный по encoding)
r.content # bytes (raw)
r.json() # dict (парсит JSON)
# Заголовки
r.headers # dict-like, case-insensitive
r.headers["Content-Type"] # 'application/json; charset=utf-8'
# URL и метод
r.url # финальный URL после редиректов
r.history # list of intermediate Response objects если были 3xx редиректы
# Cookies
r.cookies # RequestsCookieJar
.text vs .content vs .json()
Эта тройка — источник 80 процентов багов с encoding у junior разработчиков. Пойми разницу.
.content — bytes
Сырые байты. Что пришло с сервера, то и здесь. Подходит для:
- Бинарных файлов (изображения, PDF, Parquet, ZIP).
- Когда сам хочешь декодировать (знаешь encoding точнее, чем сервер заявил).
r = requests.get("https://example.com/report.pdf")
with open("report.pdf", "wb") as f:
f.write(r.content)
.text — str
Декодированная строка. requests смотрит на:
Content-Typeheader ->charset=...(например,text/html; charset=utf-8).- Если charset не указан — пытается угадать через
charset-normalizer(или old:chardet). - Fallback —
ISO-8859-1(latin-1) согласно HTTP/1.1 RFC.
r = requests.get("https://example.com/page.html")
print(r.encoding) # 'utf-8' (или то что определилось)
print(r.text[:100])
Проблема: если сервер не указал charset, requests может угадать неверно. Решение — задать вручную:
r = requests.get("https://example.com/page.html")
r.encoding = "utf-8" # форсированно
print(r.text)
.json() — dict/list
Парсит body как JSON. Под капотом — json.loads(r.content) (с правильным encoding). Бросает requests.exceptions.JSONDecodeError если body не JSON.
r = requests.get("https://api.example.com/users")
try:
data = r.json()
except requests.exceptions.JSONDecodeError:
print("Сервер вернул не JSON:", r.text[:200])
Никогда не парсь JSON через json.loads(r.text) — это лишний цикл декод-кодирования (bytes -> str -> bytes -> dict). Просто r.json() — он работает с .content напрямую.
json.loads / json.dumps — стандартная библиотека Python
.raise_for_status() — быстрый exception на ошибочные статусы
r = requests.get("https://api.example.com/users/9999")
r.raise_for_status()
# Если status >= 400 -- кидает requests.exceptions.HTTPError
# Иначе -- ничего не делает, продолжаем
data = r.json()
Стандартный pattern для production кода — обернуть в try/except:
try:
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.json()
except requests.exceptions.HTTPError as e:
# 4xx или 5xx
print(f"HTTP {e.response.status_code}: {e.response.text}")
except requests.exceptions.ConnectionError:
print("Сетевая ошибка")
except requests.exceptions.Timeout:
print("Таймаут")
except requests.exceptions.JSONDecodeError:
print("Не JSON")
requests и httpx: HTTP-клиенты Python
Streaming для больших файлов
По умолчанию requests скачивает весь body в память до возврата из get. Для CSV на 10 ГБ это убийственно — OOM. Решение — stream=True:
import requests
with requests.get("https://example.com/huge.csv", stream=True) as r:
r.raise_for_status()
with open("huge.csv", "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
Что происходит:
stream=True— заголовки получены, body НЕ загружен в память.iter_content(chunk_size=8192)— итерируем по 8KB-чанкам с сервера.- Записываем по чанкам в файл — память не растёт.
Для текста по строкам (например, JSONL):
with requests.get("https://example.com/events.jsonl", stream=True) as r:
for line in r.iter_lines():
if line: # пропускаем пустые
event = json.loads(line)
process(event)
В режиме stream=True соединение НЕ закрывается до полного прочтения body. Используй with блок (как выше) или явно r.close() чтобы вернуть TCP-соединение в pool. Иначе — утечка соединений и в итоге Too many open files.
Попробуй сам
Поработай с публичным API:
import requests
# 1. GET список постов
r = requests.get("https://jsonplaceholder.typicode.com/posts", timeout=10)
r.raise_for_status()
posts = r.json()
print(f"Got {len(posts)} posts") # Got 100 posts
print(posts[0].keys()) # dict_keys(['userId', 'id', 'title', 'body'])
# 2. GET с фильтрами через params
r = requests.get(
"https://jsonplaceholder.typicode.com/posts",
params={"userId": 1},
timeout=10
)
print(f"User 1 posts: {len(r.json())}") # User 1 posts: 10
# 3. POST новый пост
new_post = {"title": "REST API course", "body": "Learning requests", "userId": 1}
r = requests.post(
"https://jsonplaceholder.typicode.com/posts",
json=new_post,
timeout=10
)
print(r.status_code) # 201
print(r.json()) # созданный пост с присвоенным id
# 4. Streaming большого ответа
url = "https://api.github.com/users" # большой JSON
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
print("First 4KB:")
print(next(r.iter_content(4096)).decode("utf-8")[:500])
Сделай простой скрипт: скачай 10 постов с JSONPlaceholder, для каждого скачай его комментарии (/posts/{id}/comments), сохрани результат в JSONL файл.
Killer takeaway
requests 2.34 — стандарт HTTP-клиента в Python с 2011 года, 300+ млн скачиваний в месяц. Все методы — функции верхнего уровня (get/post/put/patch/delete/head/options). Главные kwargs: params (query string), json (JSON body + Content-Type), data (form-encoded или raw), headers (custom), files (multipart). Response: .status_code, .text (str с encoding), .content (bytes), .json() (parse). Encoding-проблемы -> форсировать r.encoding = 'utf-8'. Для больших файлов — stream=True + iter_content(chunk_size) и обязательно with блок чтобы вернуть соединение в pool.