Learning Platform
Глоссарий Troubleshooting
Урок 07.01 · 25 мин
Начальный
requestshttp-clientpythonjsonstreaming

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=....

NOTE

Если нужно отправить и файл, и обычные 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
Response объект -- основные атрибуты
HTTP-уровневые
status_codeЧисловой статус 200/404/500
okBool: status < 400
reasonТекстовое описание: OK, Not Found
headersdict-like, case-insensitive
Body -- три представления одного и того же
content (bytes)Сырые байты как пришли с сервера
text (str)Декодированная строка, encoding по headers
json() (dict/list)Распарсенный JSON в Python dict/list
Контекст запроса
urlФинальный URL после редиректов
historyСписок промежуточных Response при 3xx
cookiesRequestsCookieJar

.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 смотрит на:

  1. Content-Type header -> charset=... (например, text/html; charset=utf-8).
  2. Если charset не указан — пытается угадать через charset-normalizer (или old: chardet).
  3. 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])
WARNING

Никогда не парсь 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)

Что происходит:

  1. stream=True — заголовки получены, body НЕ загружен в память.
  2. iter_content(chunk_size=8192) — итерируем по 8KB-чанкам с сервера.
  3. Записываем по чанкам в файл — память не растёт.

Для текста по строкам (например, 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)
WARNING

В режиме 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.

Проверка знанийKnowledge check
ОтветAnswer

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. В чём разница между r.text, r.content и r.json() в requests?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5