Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 20 из 20 Средний 45 мин 150 XP

Проект: каталог книг — от URL до файла

Финал раздела: один скрипт собирает каталог книг — параметры в URL, разбор ответа, фильтр по рейтингу, сортировка по цене и проверенный файл на диске.

Редакция Питоники

Восемнадцать уроков ты собирал инструменты по одному. Настало время склеить их в то, ради чего всё затевалось: настоящий скрипт, который можно запустить у себя на компьютере — поменять категорию, запустить снова — и получить свежий файл данных. Задача проекта: каталог фэнтези-книг с рейтингом не ниже 4.6, отсортированный по цене, — от параметров запроса до проверенного catalog.json на диске.

В проекте всё исполняется прямо на странице: URL собирается по-настоящему, ответ — сохранённый текст того вида, что отдаёт настоящий API, а фильтрация, сортировка и запись в файл — честный Python.

План проекта

  1. собрать параметры и адрес запроса через urlencode;
  2. получить ответ (в песочнице — сохранённая константа);
  3. разобрать текст ответа json.loads;
  4. отфильтровать книги по рейтингу и отсортировать по цене;
  5. собрать каталог и записать catalog.json с ensure_ascii=False и indent=2;
  6. прочитать файл обратно и проверить результат.

Шесть шагов — и ни одного нового инструмента: только то, что ты уже собирал руками по одному, по уроку на инструмент. Всё, что ниже, исполняется прямо на странице — проект можно пройти целиком, не вставая из песочницы. Разница проекта в том, что шаги едут одним скриптом, а значит, повторяются одним запуском: один прогон — один свежий каталог, без ручных операций посередине. Сохрани финальный код на свой компьютер — и каталог фэнтези обновляется командой python catalog.py; повесь на планировщик задач — и данные в файле свежие каждое утро, без единого щелчка руками. Именно так и живут настоящие сборщики данных: маленький скрипт, который просто делает свою работу по расписанию.

Шаг 1: параметры и адрес

Запрос начинается не с кода, а с вопроса: какую категорию просим, сколько книг, какой порог рейтинга. Параметры живут в словаре — и urlencode превращает их в законный query string.

шаг 1: адрес запроса
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 вокруг нас.

другая категория — другая строка params
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.

шаг 2: разбор ответа
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 расставляет их от доступных к дорогим. Витрина читается сверху вниз: сначала то, что легко купить.

шаг 3: отбор и порядок
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 для человека, обратное чтение для уверенности.

шаги 4-6: запись и проверка
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 лежат в самом верху файла — справка идёт перед данными, как титульный лист перед текстом. Вот как выглядит начало файла, если прочитать его как обычный текст:

файл глазами: первые строки catalog.json
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": "Хоббит",

Один скрипт от начала до конца

Шаги отработали по отдельности — пора увидеть проект целиком. Вот он, от первой строки до последней, без единой библиотеки сверх стандартной:

весь проект: от URL до файла
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])
Проверь себя
0 / 7

1. Зачем в проекте urlencode, если параметры можно вписать в адрес руками?

2. Что в проекте пришлось бы заменить для работы в настоящей сети?

3. Почему SAVED_RESPONSE["books"] падает с ошибкой, а json.loads(SAVED_RESPONSE)["books"] работает?

4. Зачем в словарь catalog положили url и condition?

5. Что означает True в выводе «Совпало: back == catalog»?

6. Сколько раз можно запускать проект подряд?

7. Как собрать из этого проекта каталог классики вместо фэнтези?

Карточки терминов
Запомнено: 0 / 6
Практика

Собери мини-каталог: из сохранённого ответа выбери книги дешевле 1200 рублей с рейтингом 4.5 и выше, отсортируй по цене по возрастанию и сохрани в файл my-catalog.json (ensure_ascii=False, indent=2) со ключами count и books. Прочитай файл обратно, выведи количество книг и название первой.

practice.py
Вопросы и ответы по уроку

Как написать на 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.