Проект: каталог книг — от URL до файла
Финал раздела: один скрипт собирает каталог книг — параметры в URL, разбор ответа, фильтр по рейтингу, сортировка по цене и проверенный файл на диске.
Редакция Питоники
Восемнадцать уроков ты собирал инструменты по одному. Настало время склеить их в то, ради чего всё затевалось: настоящий скрипт, который можно запустить у себя на компьютере — поменять категорию, запустить снова — и получить свежий файл данных. Задача проекта: каталог фэнтези-книг с рейтингом не ниже 4.6, отсортированный по цене, — от параметров запроса до проверенного catalog.json на диске.
В проекте всё исполняется прямо на странице: URL собирается по-настоящему, ответ — сохранённый текст того вида, что отдаёт настоящий API, а фильтрация, сортировка и запись в файл — честный Python.
План проекта
- собрать параметры и адрес запроса через urlencode;
- получить ответ (в песочнице — сохранённая константа);
- разобрать текст ответа json.loads;
- отфильтровать книги по рейтингу и отсортировать по цене;
- собрать каталог и записать catalog.json с ensure_ascii=False и indent=2;
- прочитать файл обратно и проверить результат.
Шесть шагов — и ни одного нового инструмента: только то, что ты уже собирал руками по одному, по уроку на инструмент. Всё, что ниже, исполняется прямо на странице — проект можно пройти целиком, не вставая из песочницы. Разница проекта в том, что шаги едут одним скриптом, а значит, повторяются одним запуском: один прогон — один свежий каталог, без ручных операций посередине. Сохрани финальный код на свой компьютер — и каталог фэнтези обновляется командой python catalog.py; повесь на планировщик задач — и данные в файле свежие каждое утро, без единого щелчка руками. Именно так и живут настоящие сборщики данных: маленький скрипт, который просто делает свою работу по расписанию.
Шаг 1: параметры и адрес
Запрос начинается не с кода, а с вопроса: какую категорию просим, сколько книг, какой порог рейтинга. Параметры живут в словаре — и urlencode превращает их в законный query string.
from urllib.parse import urlencode
BASE_URL = "https://api.books.example/v1/books"
params = {
"category": "fantasy",
"limit": 6,
"min_rating": 4.0,
}
print(BASE_URL + "?" + urlencode(params))
https://api.books.example/v1/books?category=fantasy&limit=6&min_rating=4.0
Три параметра — три смысла: что просим, сколько берём, с каким порогом. Поменять выборку — значит поменять словарь: category станет «classic», limit вырастет до ста, а весь остальной код проекта не шелохнётся. Именно так и проектируют запросы: параметры — конфигурация, код — механика. В настоящем API limit ещё и защита сервера: он не отдаст весь каталог одним куском, сколько ни проси, — а ты честно берёшь шесть позиций, ровно сколько лежит в сохранённом ответе.
from urllib.request import urlopen
url = BASE_URL + "?" + urlencode(params)
with urlopen(url) as response:
text = response.read().decode("utf-8")
data = json.loads(text)
Проверим гибкость проекта: смена выборки — это новый словарь параметров, а не новый код. Три категории — три адреса, механика одна: скопировал строку в адресную строку настоящего сайта — и увидишь его параметры у себя в браузере; query string вокруг нас.
from urllib.parse import urlencode
def build_url(base, params):
return base + "?" + urlencode(params)
for category in ("fantasy", "classic", "detective"):
params = {"category": category, "limit": 3, "min_rating": 4.0}
print(build_url("https://api.books.example/v1/books", params))
https://api.books.example/v1/books?category=fantasy&limit=3&min_rating=4.0 https://api.books.example/v1/books?category=classic&limit=3&min_rating=4.0 https://api.books.example/v1/books?category=detective&limit=3&min_rating=4.0
Шаг 2: ответ как данные
Ответ книжного API — словарь со статусом, категорией и списком книг. Текст лежит в константе байт-в-байт так, как его отдал бы сервер; json.loads делает из него данные Python.
import json
SAVED_RESPONSE = """
{
"status": "ok",
"category": "fantasy",
"books": [
{"title": "Хоббит", "author": "Толкин", "price": 990, "rating": 4.8},
{"title": "Гарри Поттер и философский камень", "author": "Роулинг", "price": 1150, "rating": 4.7},
{"title": "Алиса в Стране чудес", "author": "Кэрролл", "price": 640, "rating": 4.4},
{"title": "Властелин колец", "author": "Толкин", "price": 2400, "rating": 4.9},
{"title": "Хроники Нарнии", "author": "Льюис", "price": 1500, "rating": 4.6},
{"title": "Маленький принц", "author": "Экзюпери", "price": 560, "rating": 4.9}
]
}
"""
data = json.loads(SAVED_RESPONSE)
books = data["books"]
print(data["status"], data["category"])
print(len(books), "книг в ответе")
print(books[0]["title"], "-", books[0]["price"], "руб.")
ok fantasy 6 книг в ответе Хоббит - 990 руб.
Шаг 3: фильтр по рейтингу, сортировка по цене
Теперь вопросы из урока 15 и урока 16 работают в паре: фильтр оставляет книги с рейтингом не ниже 4.6, sorted расставляет их от доступных к дорогим. Витрина читается сверху вниз: сначала то, что легко купить.
import json
SAVED_RESPONSE = """
{
"status": "ok",
"category": "fantasy",
"books": [
{"title": "Хоббит", "author": "Толкин", "price": 990, "rating": 4.8},
{"title": "Гарри Поттер и философский камень", "author": "Роулинг", "price": 1150, "rating": 4.7},
{"title": "Алиса в Стране чудес", "author": "Кэрролл", "price": 640, "rating": 4.4},
{"title": "Властелин колец", "author": "Толкин", "price": 2400, "rating": 4.9},
{"title": "Хроники Нарнии", "author": "Льюис", "price": 1500, "rating": 4.6},
{"title": "Маленький принц", "author": "Экзюпери", "price": 560, "rating": 4.9}
]
}
"""
books = json.loads(SAVED_RESPONSE)["books"]
selection = [book for book in books if book["rating"] >= 4.6]
selection = sorted(selection, key=lambda book: book["price"])
for book in selection:
print(book["title"], "-", book["price"], "- рейтинг", book["rating"])
Маленький принц - 560 - рейтинг 4.9 Хоббит - 990 - рейтинг 4.8 Гарри Поттер и философский камень - 1150 - рейтинг 4.7 Хроники Нарнии - 1500 - рейтинг 4.6 Властелин колец - 2400 - рейтинг 4.9
Из шести книг остыла одна: у Алисы рейтинг 4.4. Остальные выстроились по цене — от «Маленького принца» за 560 до двухтомника Толкина за 2400, и читать витрину стало легко: глаза идут сверху вниз, кошелёк выдыхает всё спокойнее. Порядок операций выбран не случайно: фильтр сузил список, и сортировать пришлось меньше — так дешевле, а главное, понятнее: каждый шаг делает свою работу над своим списком. На большой витрине порядок становится ещё важнее: зачем сортировать тысячи позиций, чтобы выбросить девяносто процентов на следующей строке.
К выборке положен и числовой паспорт — короткая сводка, которую приятно показать заказчику или положить рядом с каталогом: сколько отобрано, средняя цена, лидер рейтинга. Всё считается на месте теми же sum, len и max с key; числа приезжают настоящими int и float — считать по ним можно сразу, без единого преобразования.
import json
SAVED_RESPONSE = """
{
"status": "ok",
"category": "fantasy",
"books": [
{"title": "Хоббит", "author": "Толкин", "price": 990, "rating": 4.8},
{"title": "Гарри Поттер и философский камень", "author": "Роулинг", "price": 1150, "rating": 4.7},
{"title": "Алиса в Стране чудес", "author": "Кэрролл", "price": 640, "rating": 4.4},
{"title": "Властелин колец", "author": "Толкин", "price": 2400, "rating": 4.9},
{"title": "Хроники Нарнии", "author": "Льюис", "price": 1500, "rating": 4.6},
{"title": "Маленький принц", "author": "Экзюпери", "price": 560, "rating": 4.9}
]
}
"""
books = json.loads(SAVED_RESPONSE)["books"]
selection = [b for b in books if b["rating"] >= 4.6]
prices = [b["price"] for b in selection]
print("Отобрано:", len(selection), "из", len(books))
print("Средняя цена:", round(sum(prices) / len(prices)))
print("Самая дорогая:", max(selection, key=lambda b: b["price"])["title"])
Отобрано: 5 из 6 Средняя цена: 1320 Самая дорогая: Властелин колец
Шаг 4: каталог в файл
Каталог — не просто список: это отчёт со справкой о себе. Внутрь едут категория, условие отбора, количество и сами книги — всё, что понадобится читателю файла, включая тебя через месяц. Запись — по правилам урока 17: ensure_ascii=False для кириллицы, indent=2 для человека, обратное чтение для уверенности.
import json, os
SAVED_RESPONSE = """
{
"status": "ok",
"category": "fantasy",
"books": [
{"title": "Хоббит", "author": "Толкин", "price": 990, "rating": 4.8},
{"title": "Гарри Поттер и философский камень", "author": "Роулинг", "price": 1150, "rating": 4.7},
{"title": "Алиса в Стране чудес", "author": "Кэрролл", "price": 640, "rating": 4.4},
{"title": "Властелин колец", "author": "Толкин", "price": 2400, "rating": 4.9},
{"title": "Хроники Нарнии", "author": "Льюис", "price": 1500, "rating": 4.6},
{"title": "Маленький принц", "author": "Экзюпери", "price": 560, "rating": 4.9}
]
}
"""
books = json.loads(SAVED_RESPONSE)["books"]
selection = sorted(
(book for book in books if book["rating"] >= 4.6),
key=lambda book: book["price"],
)
catalog = {
"category": "fantasy",
"condition": "rating >= 4.6, отсортировано по цене",
"count": len(selection),
"books": selection,
}
with open("catalog.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, ensure_ascii=False, indent=2)
with open("catalog.json", encoding="utf-8") as f:
back = json.load(f)
print("Файл создан:", os.path.exists("catalog.json"))
print("Совпало:", back == catalog)
print("Книг в каталоге:", back["count"])
print("Первая:", back["books"][0]["title"])
os.remove("catalog.json")
Файл создан: True Совпало: True Книг в каталоге: 5 Первая: Маленький принц
Четыре строки проверки — и файл подтверждает сам себя: создан, читается, содержит ровно то, что собрали. У себя на компьютере строку с os.remove убери — catalog.json останется на диске, и его можно открыть любым редактором: кириллица будет кириллицей, лесенка — лесенкой. Ключи url и condition лежат в самом верху файла — справка идёт перед данными, как титульный лист перед текстом. Вот как выглядит начало файла, если прочитать его как обычный текст:
import json, os
SAVED_RESPONSE = """
{
"status": "ok",
"category": "fantasy",
"books": [
{"title": "Хоббит", "author": "Толкин", "price": 990, "rating": 4.8},
{"title": "Гарри Поттер и философский камень", "author": "Роулинг", "price": 1150, "rating": 4.7},
{"title": "Алиса в Стране чудес", "author": "Кэрролл", "price": 640, "rating": 4.4},
{"title": "Властелин колец", "author": "Толкин", "price": 2400, "rating": 4.9},
{"title": "Хроники Нарнии", "author": "Льюис", "price": 1500, "rating": 4.6},
{"title": "Маленький принц", "author": "Экзюпери", "price": 560, "rating": 4.9}
]
}
"""
books = json.loads(SAVED_RESPONSE)["books"]
catalog = {"category": "fantasy", "count": len(books), "books": books}
with open("catalog.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, ensure_ascii=False, indent=2)
with open("catalog.json", encoding="utf-8") as f:
for number, line in enumerate(f, start=1):
print(number, line.rstrip())
if number >= 6:
break
os.remove("catalog.json")
1 {
2 "category": "fantasy",
3 "count": 6,
4 "books": [
5 {
6 "title": "Хоббит",Один скрипт от начала до конца
Шаги отработали по отдельности — пора увидеть проект целиком. Вот он, от первой строки до последней, без единой библиотеки сверх стандартной:
import json, os
from urllib.parse import urlencode
BASE_URL = "https://api.books.example/v1/books"
SAVED_RESPONSE = """
{
"status": "ok",
"category": "fantasy",
"books": [
{"title": "Хоббит", "author": "Толкин", "price": 990, "rating": 4.8},
{"title": "Гарри Поттер и философский камень", "author": "Роулинг", "price": 1150, "rating": 4.7},
{"title": "Алиса в Стране чудес", "author": "Кэрролл", "price": 640, "rating": 4.4},
{"title": "Властелин колец", "author": "Толкин", "price": 2400, "rating": 4.9},
{"title": "Хроники Нарнии", "author": "Льюис", "price": 1500, "rating": 4.6},
{"title": "Маленький принц", "author": "Экзюпери", "price": 560, "rating": 4.9}
]
}
"""
# шаг 1: параметры и адрес запроса
params = {"category": "fantasy", "limit": 6, "min_rating": 4.0}
url = BASE_URL + "?" + urlencode(params)
# шаг 2: ответ как данные
data = json.loads(SAVED_RESPONSE)
# шаг 3: фильтр по рейтингу и сортировка по цене
selection = sorted(
(book for book in data["books"] if book["rating"] >= 4.6),
key=lambda book: book["price"],
)
# шаг 4: каталог в файл
catalog = {
"url": url,
"count": len(selection),
"books": selection,
}
with open("catalog.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, ensure_ascii=False, indent=2)
# шаг 5: проверка обратным чтением
with open("catalog.json", encoding="utf-8") as f:
back = json.load(f)
print("Адрес:", url)
print("Проверка:", back == catalog)
print("Книг:", back["count"], "- первая:", back["books"][0]["title"])
os.remove("catalog.json")
Адрес: https://api.books.example/v1/books?category=fantasy&limit=6&min_rating=4.0 Проверка: True Книг: 5 - первая: Маленький принц
Комментарии-шаги в коде — не украшение: через полгода они расскажут, где кончается адрес и начинается обработка, без чтения тел функций и раскапывания истории. Тридцать строк — и это законченная программа: она задаёт вопрос API, обрабатывает ответ, складывает результат на диск и отчитывается о проверке. Хочешь каталог классики с ценой до тысячи? Меняются две строки — params и условие фильтра; механика, проверки и файл остаются теми же. На компьютере добавляется urlopen из небегущего блока — и скрипт ходит в настоящую сеть.
Перед запуском у себя прогони по скрипту короткий контрольный список: у каждого open есть encoding="utf-8"? адрес собран urlencode, а не руками? файл читается обратно и сравнивается с собранным? Проверки — это не паранойя, а способ скрипта отчитаться о работе. А когда захочется закрепить отчёт тестом, который ловит поломку до пользователя, — этому посвящён урок про тесты для JSON-файлов курса pytest.
Что ты теперь умеешь
Раздел начался с первой строки json.dumps({...}) — и вот ты здесь. Оглянемся:
- превращать словари и списки в JSON-текст и обратно: dumps и loads;
- читать и писать json-файлы с правильной кодировкой и кириллицей без кракозябр;
- оформлять данные для людей: indent, sort_keys, ensure_ascii=False;
- разбирать вложенные структуры и добираться до значений в глубину;
- ловить JSONDecodeError и отличать битый текст от пустого;
- собирать адрес запроса через urlencode и parse_qs из urllib.parse;
- фильтровать, считать средние и доли, строить топ-N;
- сохранять результаты с метаданными и проверять их обратным чтением;
- выбирать между JSON и CSV и конвертировать данные туда и обратно.
Данные перестают пугать, когда у тебя есть план: URL, ответ, фильтр, сортировка, файл — и каждый шаг проверяется. Любая выгрузка, любой ответ API теперь не стена, а список из знакомых шагов — открой, посмотри структуру, задай вопросы, сохрани результат. План работает и на чужих данных: незнакомый JSON из настоящего API разбирается тем же приёмом — печать структуры, обход сверху, фильтр по полям. Дальше по вкусу: курс requests научит доставлять ответы по-настоящему, pandas — считать по таблицам, как Excel на стероидах, а повторение этих уроков в песочнице — бесплатно и без установки.
Каталог собран, файл проверен, план на руках: данные — это шаги, а шаги ты умеешь.
Сначала предскажи ответ в голове — это главный навык программиста.
from urllib.parse import urlencode
params = {"category": "fantasy", "limit": 3}
print(urlencode(params))
import json
text = '{"books": [{"t": "А", "p": 100}, {"t": "Б", "p": 300}]}'
books = json.loads(text)["books"]
cheap = [b["t"] for b in books if b["p"] < 200]
print(len(books) - len(cheap))
import json
catalog = {"count": 2, "books": ["А", "Б"]}
with open("oq.json", "w", encoding="utf-8") as f:
json.dump(catalog, f, ensure_ascii=False)
with open("oq.json", encoding="utf-8") as f:
back = json.load(f)
print(back == catalog, back["books"][1])
1. Зачем в проекте urlencode, если параметры можно вписать в адрес руками?
2. Что в проекте пришлось бы заменить для работы в настоящей сети?
3. Почему SAVED_RESPONSE["books"] падает с ошибкой, а json.loads(SAVED_RESPONSE)["books"] работает?
4. Зачем в словарь catalog положили url и condition?
5. Что означает True в выводе «Совпало: back == catalog»?
6. Сколько раз можно запускать проект подряд?
7. Как собрать из этого проекта каталог классики вместо фэнтези?
Собери мини-каталог: из сохранённого ответа выбери книги дешевле 1200 рублей с рейтингом 4.5 и выше, отсортируй по цене по возрастанию и сохрани в файл my-catalog.json (ensure_ascii=False, indent=2) со ключами count и books. Прочитай файл обратно, выведи количество книг и название первой.
Как написать на Python скрипт, который забирает данные из API и сохраняет в файл?
Схема из шести шагов: urlencode собирает адрес с параметрами, urlopen получает ответ, json.loads разбирает текст, фильтр и sorted готовят выборку, json.dump пишет файл с ensure_ascii=False и indent=2, обратное чтение проверяет результат. В этом проекте все шаги разобраны на живом примере — каталоге книг.
Можно ли запускать этот проект без интернета?
Да, вся обработка — стандартная библиотека и файл на диске. Сеть нужна только для получения ответа: в уроках он сохранён в константу, на компьютере заменяется urlopen(url). Такой приём удобен и для разработки: работаешь с зафиксированным ответом, а сеть подключаешь в конце.
Как расширить проект под другую выборку?
Изменить словарь params: category, limit, min_rating — и при желании условие фильтра. Весь остальной код не меняется: параметры — конфигурация, механика — код. Это главный принцип проекта: собери один раз, меняй вопросы к данным.
Что изучать после курса JSON?
Курс requests — доставлять ответы по-настоящему: параметры, заголовки, коды ответа и настоящий API-клиент. Pandas — считать по таблицам: группировки, сводные, миллионы строк. Оба курса опираются на умение работать с JSON, которое ты уже собрал.
Что делать, если API вернул пустой список книг?
Проект продолжит работать честно: фильтр вернёт пустой список, count окажется нулём, а в файл запишется корректный каталог без книг. Вывод вроде «Первая: ...» стоит обернуть проверкой if selection: — печатать первую книгу только когда она есть. Пустая выборка — не ошибка, а данные: сервер честно сказал «под условие ничего нет».
Понравился урок? Сошлитесь на него
«Данные перестают пугать, когда у тебя есть план: URL, ответ, фильтр, сортировка, файл — и каждый шаг проверяется.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
json · Урок 19
Мини-клиент API на функциях
URL, разбор ответа, фильтр, файл — каждый шаг уже умещается в одну функцию. Собираем их в клиент, который читается как сценарий.
json · Урок 17
Сохраняем результаты: json.dump итоговых данных
Отфильтровали, отсортировали, посчитали — теперь результат должен пережить скрипт: json.dump пишет итог в файл с кириллицей и лесенкой.
requests · Урок 10
Проект: API-клиент на requests с таймаутами, повторами и кэшем
Финальный проект: собираем всё, что знаем про requests, в один класс WeatherClient — таймаут, экспоненциальные повторы, кэш и аккуратные исключения.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
BeautifulSoup / Scrapy · Урок 7
Сохранение результатов: CSV, JSON и базы данных
Список словарей собран — теперь доведём его до файла: CSV для таблиц, JSON для вложенных структур, sqlite3 для больших объёмов. Плюс две классические ловушки: пустые строки и кракозябры в Excel.
как сохранить результаты парсинга в csv jsonjson dump python
sqlite3 · Урок 10
Проект: база книжной библиотеки на SQLite с отчётами SQL
Финальный проект курса: база библиотеки из трёх связанных таблиц — авторы, книги, выдачи — и четыре настоящих отчёта на SQL. Собираем всё, что выучили за девять уроков.
sqlite python проектбаза данных книг sqlite
json · Урок 4
Файлы JSON: json.dump и json.load
Данные, которые переживают скрипт: json.dump пишет словарь в файл, json.load читает обратно, а round-trip подтверждает — сохранил, прочитал, совпало.
json файл python сохранитьjson.dump python