Собираем URL для API: urllib.parse
Схема, хост, путь, query: urlparse разбирает адрес, urlencode кодирует кириллицу в проценты, parse_qs читает строку запроса обратно — и весь запрос к API собирается без сети.
Редакция Питоники
До сих пор наши данные жили в переменных. Пора идти за настоящими — а настоящий API-запрос начинается не с сети, а с правильно собранного адреса. Фильтрация из прошлого урока учит сервер делать за нас: не качать все заказы и не отсеивать дома, а сразу попросить нужное параметрами в URL. Умение собрать такой адрес — половина работы с любым API.
Инструмент — стандартный модуль urllib.parse, и он полностью исполняется в браузерной песочнице: разбор и сборку адресов ты запускаешь кнопкой на странице, сеть для этого не нужна. Сеть понадобится позже — когда URL уже будет готов.
Анатомия URL: четыре части
Разберём адрес запроса на части. Схема — как говорить с сервером; хост — к какому серверу; путь — какой у него раздел; query — то, что после знака вопроса: параметры через амперсанд. Функция urlparse делает этот разбор за нас.
from urllib.parse import urlparse
url = "https://api.books.ru/v1/search?q=python&limit=5"
parts = urlparse(url)
print("Схема:", parts.scheme)
print("Хост:", parts.hostname)
print("Путь:", parts.path)
print("Query:", parts.query)
Схема: https Хост: api.books.ru Путь: /v1/search Query: q=python&limit=5
| Часть | Пример | Зачем серверу |
|---|---|---|
| Схема | https | протокол разговора |
| Хост | api.books.ru | адрес сервера в интернете |
| Путь | /v1/search | какой раздел API звать |
| Query | q=python&limit=5 | параметры: что искать и сколько |
Query — это пары «имя=значение», разделённые амперсандом. Именно его мы и будем собирать: сервер читает параметры из query и решает, что нам отдать. Собрать его руками нельзя — сейчас увидишь почему.
Параметрами API живут на полную: фильтры (status=paid), пагинация (page=2&limit=50), язык ответа (lang=ru), ключи доступа (api_key=...). Чем понятнее ты собираешь query, тем меньше работы остаётся серверу и тем меньше мусора приезжает обратно. Это та же идея, что в фильтрации на Python, только условие выполняет сервер: не качай всё — попроси нужное.
| Атрибут urlparse | Пример | Что это |
|---|---|---|
| scheme | https | протокол |
| hostname | api.books.ru | хост без схемы |
| port | None | порт, если указан явно |
| path | /v1/search | путь на сервере |
| query | q=python&limit=5 | строка параметров |
| fragment | None | якорь после решётки |
urlencode: словарь в строку запроса
В URL разрешены не все символы: только латиница, цифры и горстка знаков. Кириллица, пробелы, амперсанды внутри значений должны быть закодированы — каждая недопустимая буква превращается в процент и шестнадцатеричный код. Функция urlencode принимает словарь и возвращает готовую строку запроса:
from urllib.parse import urlencode
params = {"city": "Москва"}
query = urlencode(params)
print(query)
city=%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0
urlencode превращает словарь параметров в честную строку запроса: кириллица и пробелы кодируются так, как ждёт сервер. %D0%9C — это «М» в кодировке UTF-8, записанная шестнадцатеричными цифрами: шесть букв «Москва» разрослись в восемнадцать символов кода, но сервер раскодирует их обратно в точности.
Почему буквы превращаются в проценты? Стандарт URL разрешает в адресе только латиницу, цифры и несколько служебных знаков — текст едет по сети в надёжной ASCII-одежде, не путаясь с разделителями query вроде амперсанда и знака равенства. Каждая кириллическая буква в UTF-8 занимает два байта, и urlencode печатает их шестнадцатеричными парами: «М» — это D0 9C, отсюда %D0%9C.
from urllib.parse import urlencode
query = urlencode({"city": "Москва", "days": 3, "lang": "ru"})
print(query)
query2 = urlencode({"q": "пицца рядом", "limit": 5})
print(query2)
city=%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0&days=3&lang=ru q=%D0%BF%D0%B8%D1%86%D1%86%D0%B0+%D1%80%D1%8F%D0%B4%D0%BE%D0%BC&limit=5
Обрати внимание на пробел: в query он живёт как знак +. Порядок параметров urlencode берёт из словаря — тот же, что ты записал. Числа он превращает в строки сам: days=3 получилось из целого числа без единого ручного str().
Значение-список urlencode тоже умеет: с параметром doseq=True каждый элемент списка превращается в отдельную пару с тем же именем. Так API принимают множественные фильтры — например, сразу два тега:
from urllib.parse import urlencode
query = urlencode({"tag": ["книги", "техника"]}, doseq=True)
print(query)
tag=%D0%BA%D0%BD%D0%B8%D0%B3%D0%B8&tag=%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D0%BA%D0%B0
Без doseq=True urlencode превратил бы список в строку ['книги', 'техника'] — с квадратными скобками и кавычками внутри, что не поймёт ни один сервер. Порядок пар urlencode берёт из словаря — в Python он совпадает с порядком записи. Порядок параметров в query на результат почти никогда не влияет, но стабильный порядок делает URL предсказуемым в логах, тестах и кэшах.
parse_qs: строка обратно в словарь
Обратная дорога нужна реже, но без неё никуда: разобрать query, который пришёл тебе в callback-ссылке или в логах. parse_qs принимает строку запроса без вопросительного знака и возвращает словарь. С одним сюрпризом: значения — списки.
from urllib.parse import parse_qs
query = "city=%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0&days=3"
params = parse_qs(query)
print(params)
print(params["city"][0])
print(params["days"][0])
{'city': ['Москва'], 'days': ['3']}
Москва
3Откуда в жизни берутся строки запроса, которые хочется разобрать? Из callback-ссылок, куда сервис возвращает пользователя со своими параметрами; из ссылок из писем — там query несёт токен подтверждения; из логов, где requests печатают для отладки. parse_qs — универсальный ключ ко всем этим строкам: одна функция вместо ручного разрезания по знакам.
Собираем полный URL запроса
Теперь всё готово для главной сборки: базовый адрес, знак вопроса, urlencode — и у нас на руках настоящий запрос к API, какой бы отправила любая библиотека. Ничего не отправляем — просто печатаем:
from urllib.parse import urlencode
base = "https://api.weather.example/v1/forecast"
params = {"city": "Москва", "days": 3}
url = base + "?" + urlencode(params)
print(url)
https://api.weather.example/v1/forecast?city=%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B0&days=3
Проверим себя полным кругом: соберём URL и тут же разберём его обратно, как это сделал бы сервер. urlparse отрежет query, parse_qs вернёт словарь — и мы должны увидеть те же значения, что закладывали.
from urllib.parse import urlencode, urlparse, parse_qs
base = "https://api.weather.example/v1/forecast"
url = base + "?" + urlencode({"city": "Сочи", "days": 5})
print(url)
parts = urlparse(url)
params = parse_qs(parts.query)
print(params["city"][0], "-", params["days"][0])
https://api.weather.example/v1/forecast?city=%D0%A1%D0%BE%D1%87%D0%B8&days=5 Сочи - 5
Круг сошёлся: «Сочи» закодировалось в проценты и раскодировалось обратно. Этот же приём — самопроверка в тестах: собрал запрос, разобрал, сверил с ожиданием. Никакой сети для такой проверки не нужно.
В реальных запросах половина параметров необязательная: пользователь мог не указать цену, страницу, фильтр. Пустые строки в query лучше не отправлять вовсе — некоторые API трактуют max= как «ноль», и фильтр молча отсекает всё. Правило: собирай словарь только из заполненных значений:
from urllib.parse import urlencode
raw = {"q": "чайник", "max": "", "page": 2}
params = {k: v for k, v in raw.items() if v != ""}
print(urlencode(params))
q=%D1%87%D0%B0%D0%B9%D0%BD%D0%B8%D0%BA&page=2
Словарное включение с условием отфильтровало пустое значение ещё на входе — в query попали только настоящие параметры. Тот же приём в обратную сторону помогает при разборе: из parse_qs достают только те ключи, что реально пришли, не гадая заранее, что сервер положит в ответ.
И правило гигиены: в query не кладут пароли и долгоживущие секретные ключи сессий. Адрес с параметрами остаётся в истории браузера, логах серверов и прокси — надёжнее передавать их в теле запроса или заголовках. Ключи доступа к публичным API вида api_key=... — исключение для учебных сервисов, но и к ним стоит относиться как к не совсем публичным строкам: не выкладывать в репозиторий и перевыпускать при утечке.
quote: кодируем кусок пути
Параметры — не единственное место для кириллицы: город может сидеть и в пути, как /city/Казань. Для одиночного куска URL есть quote — он кодирует строку целиком, без пар ключ-значение, и оставляет пробел в виде %20, как положено в пути.
from urllib.parse import quote
city = "Нижний Новгород"
print(quote(city))
print("https://geo.example/city/" + quote(city))
%D0%9D%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9%20%D0%9D%D0%BE%D0%B2%D0%B3%D0%BE%D1%80%D0%BE%D0%B4 https://geo.example/city/%D0%9D%D0%B8%D0%B6%D0%BD%D0%B8%D0%B9%20%D0%9D%D0%BE%D0%B2%D0%B3%D0%BE%D1%80%D0%BE%D0%B4
from urllib.parse import urlencode
city = "Ростов-на-Дону"
bad = "https://api.example/forecast?city=" + city
good = "https://api.example/forecast?" + urlencode({"city": city})
print("Руками:", bad)
print("urlencode:", good)
Руками: https://api.example/forecast?city=Ростов-на-Дону urlencode: https://api.example/forecast?city=%D0%A0%D0%BE%D1%81%D1%82%D0%BE%D0%B2-%D0%BD%D0%B0-%D0%94%D0%BE%D0%BD%D1%83
Вариант «руками» выглядит прилично лишь потому, что в городе не оказалось пробела или амперсанда. Добавь «Ростов-на-Дону, ТЦ» — и ручная склейка сломает запрос, а urlencode спокойно закодирует всё, включая запятую.
Дорога к настоящему запросу
URL готов — как им воспользоваться? Отправить запрос может только код с доступом к сети, а у браузерной песочницы такого доступа нет — это честное ограничение, а не упрощение. Вот как выглядит отправка на своём компьютере стандартной библиотекой:
# Только на своём компьютере: песочница без сети
from urllib.request import urlopen
from urllib.parse import urlencode
url = "https://api.weather.example/v1/forecast?" + urlencode({"city": "Москва"})
with urlopen(url) as r:
body = r.read().decode("utf-8") # тело ответа - текст
print(body)
В следующих уроках мы распечатаем этот путь дальше: что именно сервер возвращает в ответе и как разобрать его текст в данные. Ответ приходит текстом — и вся механика разбора, как и сегодня, исполняется в песочнице честно.
Функция сборки запроса
Оформим сборку как функцию — так её удобно звать для разных разделов API, а сам приём **params принимает любые параметры без длинных словарей в аргументах.
from urllib.parse import urlencode, urlparse, parse_qs
def build_url(base, **params):
return base + "?" + urlencode(params)
url = build_url("https://api.shop.example/v1/products", category="книги", limit=5)
print(url)
params = parse_qs(urlparse(url).query)
print(params["category"][0], params["limit"][0])
https://api.shop.example/v1/products?category=%D0%BA%D0%BD%D0%B8%D0%B3%D0%B8&limit=5 книги 5
Одна функция — и любой запрос собирается в одну строку: адрес базы плюс сколько угодно именованных параметров. Это готовый кирпич для будущих скриптов: обернёшь в запрос — получишь клиент, обернёшь в тест — получишь самопроверку.
Есть у query и физический потолок: адрес длиннее пары тысяч символов некоторые серверы и прокси отрезают молча. Десяток параметров — нормально; словарь на сотню значений — повод задуматься о другом методе запроса, где данные едут в теле, а не в адресе. Для учебных и большинства рабочих API лимита хватает с запасом.
Готовая функция сборки окупается сразу: URL можно печатать в лог перед отправкой — и отлаживать запросы глазами, не открывая сетевой трассировщик. Половина «мистических» ошибок API оказывается опечаткой в параметре, которую печать URL показывает мгновенно.
Хороший запрос к API рождается до сети: разобранная анатомия адреса, urlencode для данных и parse_qs для самопроверки.
Сначала предскажи ответ в голове — это главный навык программиста.
from urllib.parse import urlencode
print(urlencode({"page": 2, "tag": "книги"}))
from urllib.parse import parse_qs
p = parse_qs("a=1&b=x&b=y")
print(len(p), p["b"][1])
from urllib.parse import urlparse
u = urlparse("https://api.site.ru/v2/items?page=1")
print(u.hostname, u.path)
1. Что делает urlencode({"page": 2})?
2. Что вернёт parse_qs("a=1&b=x")?
3. Куда urlencode денет пробел в значении «пицца рядом»?
4. params = parse_qs(query). Как достать город?
5. Что содержит urlparse(url).query?
Собери URL поиска по базе товаров: база https://api.shop.example/v1/search и два параметра — q со значением «чайник» и max со значением 3000. Выведи URL, затем разбери его через urlparse и parse_qs и выведи параметр q и параметр max одной строкой.
Как добавить параметры к URL запроса в Python?
Через urlencode из стандартного модуля urllib.parse: url = base + "?" + urlencode({"q": "запрос", "limit": 10}). Функция сама закодирует кириллицу, пробелы и служебные символы. Обратный разбор — urlparse(url).query и parse_qs.
Почему кириллица в URL превращается в проценты?
Стандарт URL разрешает только латиницу, цифры и несколько знаков; всё остальное кодируется процентом и шестнадцатеричным кодом байта UTF-8. %D0%9C — это «М». Сервер обязан раскодировать такие последовательности обратно, поэтому передача кириллицы через urlencode безопасна.
Чем urlencode отличается от quote?
urlencode кодирует словарь параметров в целую строку «ключ=значение&ключ=значение», а quote — одну строку целиком, для куска пути. В query пробел становится плюсом, в quote — %20: поэтому для параметров берут urlencode, для путей — quote.
Как отправить собранный URL на сервер?
Кодом с доступом к сети: urlopen из urllib.request или requests.get на своём компьютере. Браузерная песочница самоучителя сети не имеет, но сборка и разбор URL работают прямо на странице — а механику ответа разбираем в следующих уроках на сохранённом тексте.
Понравился урок? Сошлитесь на него
«urlencode превращает словарь параметров в честную строку запроса: кириллица и пробелы кодируются так, как ждёт сервер.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
json · Урок 11
Фильтрация данных: найти нужное в JSON
Включение с условием для отбора, next с default для первого подходящего, any и all для вопросов ко всей коллекции — работаем с выгрузкой заказов.
json · Урок 13
Что отдаёт API: ответ как текст
API отвечает текстом со статусом и заголовками: берём заранее сохранённый настоящий ответ — и учимся мерить его длину, искать подстроки и превращать байты в строку.
json · Урок 14
json.loads на ответе API: от текста к данным
Текст, json.loads, словарь, поля: собираем полный конвейер разбора ответа API — с проверкой структуры, обработкой пустого и битого тела.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
requests · Урок 2
Параметры GET-запроса: params и query string в requests
Query string глазами Python: словарь params превращается в ?city=Moscow&days=3, русские буквы — в проценты, а parse_qs разворачивает всё обратно. Каждый шаг исполняется на странице.
query stringпараметры get запроса python
Pandas · Урок 2
Чтение файлов в Pandas: read_csv, read_excel и знакомство с данными
read_csv со всеми нужными параметрами и примерами: разделители, кодировки с кириллицей, чтение куска большого файла и первые ошибки, которые вы увидите на практике.
encoding pandas кириллицаread_csv параметры
json · Урок 1
Что такое JSON и зачем он Python: первый json.dumps
Первое превращение словаря в json-строку одной командой: import json, json.dumps и честный взгляд на кракозябры в выводе — всё исполняется прямо на странице.
json.dumps кириллица