Или как стать героем в глазах одногруппников.

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

Страница отчета: условие задачи и код с подсветкой
Страница отчета: условие задачи и код с подсветкой

Откуда взялась проблема

Смотришь на название дисциплины «Алгоритмы и структуры данных» и думаешь, что все будет супер. А потом выясняется: по каждому разделу курса на Stepik нужно сдать отчет. И ты такой: «Ну #₽@&*».

Задач в каждом разделе немало (за семестр набирается около 400), а в отчете для каждой должны быть условие, код решения и подпись к нему – и все это по ГОСТу.

«Окей, – думаю я, – вроде не так уж сложно». И я сел делать. Открыл задачу, скопировал условие, переключился на решение, скопировал код, вставил в Word, подписал, пронумеровал – и все по новой. Через какое-то время я поймал себя на том, что сижу и делаю это как машина: руки сами жмут одни и те же сочетания клавиш, а голова вообще не участвует. Один отчет съедал около двух часов, а их впереди был не один. Тут и пришла мысль: если я работаю как машина, пусть этим занимается машина. Так появился он – Тайлер Дерден мой проект по автоматизации отчетов, «Отчет Creator».

Подход к задаче

Писать решил на Python: он лаконичный, а код на нем легко читать. Заодно хотел пощупать пакетный менеджер uv – проект и начинался как знакомство с ним. Для начала я ответил себе на три вопроса:

  1. Где брать условия задач?

  2. Как вставлять код решения, чтобы не съехала верстка?

  3. Как собрать из этого документ 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 – пробуйте. А если у вас есть свой курс или идея, как это улучшить, – пишите в комментариях или присылайте пулреквесты в репозиторий.