Или как стать героем в глазах одногруппников.
TL;DR. Скрипт на Python забирает условия задач и мои решения через API Stepik, оформляет их по ГОСТу и собирает готовый .docx. Один отчет раньше занимал около двух часов, теперь – около двух секунд. Код – на GitHub.

Откуда взялась проблема
Смотришь на название дисциплины «Алгоритмы и структуры данных» и думаешь, что все будет супер. А потом выясняется: по каждому разделу курса на Stepik нужно сдать отчет. И ты такой: «Ну #₽@&*».
Задач в каждом разделе немало (за семестр набирается около 400), а в отчете для каждой должны быть условие, код решения и подпись к нему – и все это по ГОСТу.
«Окей, – думаю я, – вроде не так уж сложно». И я сел делать. Открыл задачу, скопировал условие, переключился на решение, скопировал код, вставил в Word, подписал, пронумеровал – и все по новой. Через какое-то время я поймал себя на том, что сижу и делаю это как машина: руки сами жмут одни и те же сочетания клавиш, а голова вообще не участвует. Один отчет съедал около двух часов, а их впереди был не один. Тут и пришла мысль: если я работаю как машина, пусть этим занимается машина. Так появился он – Тайлер Дерден мой проект по автоматизации отчетов, «Отчет Creator».
Подход к задаче
Писать решил на Python: он лаконичный, а код на нем легко читать. Заодно хотел пощупать пакетный менеджер uv – проект и начинался как знакомство с ним. Для начала я ответил себе на три вопроса:
Где брать условия задач?
Как вставлять код решения, чтобы не съехала верстка?
Как собрать из этого документ Word?
С третьим все оказалось просто: для работы с .docx есть пакет python-docx.
На первый вопрос было два ответа: парсить страницы (не хотелось) или найти API (хотелось). Я вбил в поиск «stepik api» – и о чудо, API открытый. Документация – по сути сырые описания JSON, но для моей задачи этого хватило.
Сложнее всего было со вторым вопросом. Первая мысль – открыть страницу с решением в браузере через Selenium, сделать скриншот и обрезать. Звучало страшно, сложно и нудно, поэтому я стал искать путь проще (спойлер: нашел, а после комментариев к первой версии статьи – еще один, правильнее, см. раздел «Обновление»).
Решение
Весь проект – цепочка из четырех шагов: достать данные из API, разобрать условие, превратить решение в красивый кусок документа и собрать все в .docx. Разберем по порядку.
Достаем данные из Stepik
Структура у Stepik такая: курс → разделы (sections) → уроки (lessons) → шаги (steps). Задачи с кодом – это шаги с блоком типа code. Чтобы не ковыряться в словарях, я описал ответы API моделями Pydantic: они же заодно валидируют JSON. Получилось скучно: по паре полей на каждую сущность (Lesson, Step, Block и т. д.), поэтому код здесь показывать не буду – он в репозитории.
Самое интересное – свои решения: для них нужен токен. Stepik выдает его по OAuth2: на странице stepik.org/oauth2/applications создаем приложение с типом клиента confidential и grant type client-credentials, получаем client_id и client_secret и кладем их в .env. Дальше токен получается одним запросом:
def get_session() -> requests.Session: config = load_config() # CLIENT_ID и CLIENT_SECRET из .env auth = requests.auth.HTTPBasicAuth(config.client_id, config.client_secret) response = requests.post( "https://stepik.org/oauth2/token/", data={"grant_type": "client_credentials"}, auth=auth, ) token = response.json().get("access_token") if not token: raise SystemExit("Unable to authorize with provided credentials") session = requests.Session() session.headers = {"Authorization": f"Bearer {token}"} return session
С этой сессией эндпоинт /api/submissions?step={id} отдает все мои посылки по задаче. Берем последнюю со статусом correct:
def get_solution_code(self, step_id: int) -> str | None: submission_json = self._get(f"{API_URL}/submissions?step={step_id}") submission_resp = SubmissionResponse.model_validate(submission_json) for submission in reversed(submission_resp.submissions): if submission.status == "correct": return submission.reply.code return None
Разбираем условие
Условие приходит в виде HTML, так что пришлось немного попарсить его через BeautifulSoup. Логика простая: заголовок берем из <h2>, а описание – это абзацы до «Формата входных данных». Попутно выяснилось, что у задач повышенной сложности заголовка нет, – в отчет я их не включаю.
Первая версия: код картинкой
Получив строку с решением, я вбил в поиск «str to png python» и познакомился с Pillow. Рисуем текст моноширинным шрифтом JetBrains Mono на белом фоне – никакого браузера, Selenium и скриншотов. Сначала меряем textbbox, сколько места займет код, потом рисуем на холсте нужного размера. Дальше картинка вставляется в документ с подписью «Рисунок 2.N». На это я написал тесты, чтобы убедиться, что картинки получаются корректными.
Собираем документ
Вся верстка живет в WordClient на python-docx. Главный трюк для ГОСТа – не настраивать шрифты и отступы кодом, а взять за основу свой старый отчет: титульник, введение, стили заголовков и абзацев подтягиваются из шаблона. А если у лабы свое введение и цель, рядом кладется assets/<номер раздела>-template.docx – и для этого раздела берется он. main.py просто связывает все вместе: спрашивает номер раздела, проходит по урокам и складывает решения в документ с правильной нумерацией.
Запуск
Нужны Python 3.13+ и uv:
git clone https://github.com/viteax/stepik-report-creator.git cd stepik-report-creator uv sync cp .env.example .env # вписываем CLIENT_ID и CLIENT_SECRET uv run main.py 7 # 7 – номер раздела (лабы)
Готовый отчет появится в my_docs/. Другой курс – --course <id>, код картинками по-старому – --images.
Обновление: код текстом, а не картинкой
Главная претензия в комментариях – «код рисунками – брр». Справедливо: картинку нельзя скопировать, ее не найти поиском по документу, и выглядит она хуже текста. Изначально я думал, что python-docx такое не умеет, но @milssky подсказал решение: как и в обычном Word, достаточно завести отдельный стиль для кода. А подсветку синтаксиса дает pygments, который советовал @Andrey_Solomatin.
Сначала создаем стиль Code – моноширинный Courier New (он есть на любом компьютере, в отличие от JetBrains Mono), одинарный интервал, без красной строки и отступов между абзацами.
Затем разбиваем код на токены и каждый пишем отдельным фрагментом (run) со своим цветом. Переносы строк python-docx сам превращает в разрывы строки, так что весь листинг – один абзац:
from docx.shared import RGBColor from pygments import lex from pygments.lexers import PythonLexer from pygments.styles import get_style_by_name PYGMENTS_STYLE = get_style_by_name("friendly") def add_code(self, code: str) -> None: p = self.doc.add_paragraph(style=CODE_STYLE) for token_type, value in lex(code.strip("\n"), PythonLexer()): token_style = PYGMENTS_STYLE.style_for_token(token_type) run = p.add_run(value) if token_style["color"]: run.font.color.rgb = RGBColor.from_string(token_style["color"]) run.bold = token_style["bold"] run.italic = token_style["italic"]
Подпись «Рисунок» при этом меняется на «Листинг» и ставится над кодом, а чтобы она не осталась одна внизу страницы, абзацу включается keep_with_next. Старый режим с картинками я оставил за флагом --images – вдруг кому-то так привычнее.
В комментариях предлагали и другие варианты:
Вариант | Плюсы | Почему не подошел |
|---|---|---|
LaTeX + minted (@sogonov) | Лучшая подсветка, идеальная верстка | Кафедра принимает только .docx |
pandoc | Markdown → .docx одной командой | Сложнее точно попасть в шаблон ГОСТа |
Sphinx (@Andrey_Solomatin) | Сразу PDF и HTML | Нужен .docx, и для одного отчета это тяжеловато |
Спасибо всем, кто подсказал!
Что в итоге
Отчет по разделу собирается за ~2 секунды вместо ~2 часов.
Проектом пользуются около 60 человек – две группы по 30.
Чего пока нет: подсветка только для Python, задачи без заголовка в условии пропускаются, формулы из условий упрощаются до текста.
Если коротко, то получилось вот что: отчет, на который раньше уходило два часа монотонной работы, собирается за пару секунд, а оформление при этом не страдает. Сам я после этого перестал воспринимать скучную рутину как неизбежность: если я делаю одно и то же в третий раз, значит, пора что-то писать.
Проект рос из знакомства с uv, а получился инструментом, которым пользуются около 60 человек. Дальше я хочу сделать его не только под один курс. Сейчас курс можно сменить флагом --course, но разбор условий и шаблон заточены под структуру курса по Python, и на другом курсе все может поехать. В планах – научить программу работать с любыми курсами Stepik: настраиваемый формат условий, свои шаблоны и подписи.
Если вы учитесь по курсам на Stepik – пробуйте. А если у вас есть свой курс или идея, как это улучшить, – пишите в комментариях или присылайте пулреквесты в репозиторий.

