Как-то раз мне захотелось сделать для курса на платформе Эквио полный конспект всех текстовых материалов, чтобы удобно их перечитывать на досуге, так и родилась мысль, которая вылилась в небольшой инструмент для сбора данных, их обработки и создания pdf-файлов по материалам курса.  Все началось с того, что в компании, где я сейчас работаю, была запущена программа обучения для руководителей на платформе Эквио.  Википедия говорит нам, что «Эквио — мобильная платформа, единое цифровое пространство для обучения, управления и мотивации персонала. Позволяет создавать обучающие курсы, проводить онлайн-презентации, организовывать обучение сотрудников.»  Программа представляет из себя набор модулей, состоящих из видео, статей, тестов и практических заданий. В общем, добротный курс с полезными материалами. И случается, что порой хочется вернуться к какому-то материалу и перечитать его или найти нужный отрывок, который «кажется, где-то там был...». И все бы ничего, но находить нужный контент на платформе совсем непросто. Сначала мне как пользователю доступен верхний уровень, который говорит, что у меня есть модуль, который включает в себя N программ обучения, внутри которых есть секции (которые изначально все свернуты), и только развернув каждую, я могу посмотреть, какие там материалы были и уже далее открыть контент конкретного материала. Поиска по содержимому тоже не завезли. К тому же материалы курса доступны только на самой платформе и чем-то полезным непросто поделиться с коллегой, который курс не проходил. И для сравнения мне в голову пришел другой пример. Однажды довелось мне проходить курс на Coursera, так вот там у каждого модуля, кроме видео-обучалок и прочего давали в дополнительных материалах скачать информативную PDF-ку, где уже текстом было описано содержимое модуля. Эдакие методички, как в университете были, может этим меня формат и подкупает. Так к этим файлам я неоднократно возвращался, и зачастую это было удобно и полезно.  Так, обуздав свои порочные желания, попробуем понять, чего же мы хотим: Быстро находить нужное место в материалах по содержанию или по ключевым словам, если мы их запомнили Иметь у себя материалы-методички, чтобы не лазить каждый раз на сайт и не зависеть от доступности курса на платформе или самой платформы Делиться нужным разделом с коллегой, которого курс обошел стороной Сложив два плюс два, мне показалось, что организовать подобное и здесь будет хорошей идей. В голове нарисовалась схема, где я соберу текстовые материалы с сервиса, сформирую на их основе опрятные файлы с оглавлением, а их уже затем переведу в PDF. Подумано - сделано. Сначала нужно было понять, как мы сами эти материалы сможем получить. Скрестив пальцы и понадеясь, что мне не придется все же заниматься веб-скраппингом, я вооружился старым-добрым Charles и пошел смотреть, а как у Эквио вообще ходят данные. Сперва самое интересное: как отдается на фронт текст материалов?  Обратим внимание, что материалы у нас бывают разные. И для наших целей нас интересуют сейчас только "лонгриды". Изначально ожидал, что в лучшем случае текстовый контент я найду в JSON виде строки с куском HTML внутри или просто в виде строк с сырым текстом, однако результат порадовал еще больше: с бэкэнда отдается статья в markdown-разметке.  Такой поворот событий воодушевляет: значит, получить контент без приключений мы все же сможем, к тому же уже с готовым оформлением (приятный бонус). Markdown мы легко сможем сконвертировать в HTML, который затем уже переведем в PDF.  Однако, чтобы получить такой ответ, нам необходимо в запросе отправить параметры "longread" и "page", о которых мы пока не имеем представления. Поэтому разберемся с остальными запросами и раскрутим цепочку до конца. Пройдемся по порядку и изобразим схему, как клиент (браузер) взаимодействует с серверной частью. Далее для интересующихся описан более подробно разбор взаимодействия. Сначала мы открываем список всем программ обучения и уходит запрос GET /v40/learning-programs?page=1&module_id={module_id} Сервер нам отвечает массивом с "learning_programs". У каждой  программы есть название, массив "sections", а у каждой секции есть массив "materials". Далее отправляется запрос POST /v40/materials-cr?page=1, в теле мы передаем массив  "materials", содержащий id материалов, массив сформирован из материалов одной секции. В ответе получаем детальную информацию по каждому из переданных materials, из полезного здесь мы можем получить тип каждого материала, который нам пригодится далее. Затем для лонгрида отправляется POST /v40/materials/longreads/pages/titles?page=1. В теле содержится массив из одного элемента с id материала-лонгрида. Мы с вами внимательные и здесь замечаем, что, поскольку мы открываем конкретный материал, то в запросе передается всего один элемент массива, однако в предыдущем запросе мы видели такой же принцип и можем предположить, что запрашивать лонгриды можно не по одному, а сразу пачкой. К проверке этого предположения вернемся далее. Ответ здесь сообщает UUID страницы лонгрида, который также нужен, чтобы получить искомый текст. И после этого уже идет запрос POST /v40/materials/longreads/page.  В теле передается id лонгрида и UUID его страницы. В ответе и получаем нужный нам текст. Дело раскрыто, пора переходить к следующему шагу. Выделим основные шаги, которые намечаются к разработке: В качестве ЯП выбрал Python, просто потому что интересно наработать практику на нем, объем данных в моем случае небольшой, записывать полученные сырые данные никуда не будем, чтобы не усложнять реализацию, а будем держать их в памяти. Преобразуем диаграмму последовательности, которую мы составили ранее, в более подходящую для нас: Для улучшения процесса проверили догадку, которая появилась ранее: передали несколько id в массиве "longreads" и получили UUID сразу по всем. Это позволило не отправлять запрос для получения данных по каждому лонгриду, как это делает клиентская часть, а получать данные сразу для нужного количества. Начнем с аутентификации, на ней было принято волевое решение сэкономить и не писать ее вовсе. Повлиял на это также нюанс, что платформа допускает только одну активную сессию, и при новой завершает прочие сеансы. Опять смотрим запросы к серверу и видим, что используется Bearer-токен в заголовке Authorization,  значит будем использовать его. Сформируем общие заголовки для всех запросов и, чтобы наши запросы были больше похожи на настоящие и не светили в логах каким-нибудь "python-requests/2.26.0", добавим еще случайный User agent. Далее нам нужно получить данные программ обучения. Используя схему, описанную выше, получим структуру каждой программы и сложим данные в словарь вида Кроме названий и id, полезным оказалось использование order из тела ответа, на основе этого ключа сделаем далее сортировку, чтобы материалы были в том же порядке, что и на платформе. Затем для каждой программы определим типы материалов, для материалов с типом "лонгрид" получим UUID страниц и сложим это в массив, по которому далее будем получать тексты И полученные тексты соберем их в отдельном массиве со структурой Далее отфильтруем материалы в общем словаре с программами обучения на основе массива с лонгридами, чтобы оставить только материалы-лонгриды. Теперь сделаем общие markdown-файлы с контентом. Программы обучения с платформы являются верхним уровнем в структуре (не включая сам курс), поэтому будем делать файл для каждой программы обучения. Тексты отдельных лонгридов у нас уже есть, теперь их нужно собрать вместе. Для этого просто сконкатенируем их, добавляя перед каждым лонгридом его название в виде markdown-заголовка 1 уровня. Однако, поскольку в статьях уже были свои заголовки, я столкнулся с тем, что на 1 уровне были и добавляемые заголовки лонгридов, и уже имеющиеся заголовки статей. Помимо неудобств в оформлении, это помешает нам дальше при формировании оглавления. Соответственно появилась необходимость "сдвинуть" все исходные заголовки на уровень вниз, чтобы на 1 уровне оставались только те, которые мы добавляем сами. Поискав существующие наработки на эту тему, я нашел только расширение для VS Code, в исходниках которого ничего элегантного тоже не было. Поэтому я реализовал свой небольшой велосипед для смещения заголовков: Так получаем markdown-файл: Дальше нужно сделать оглавление и сконвертировать markdown в HTML. К этому моменту у меня были в голове мысли, как я смогу сделать оглавление: я хотел использовать якорные ссылки markdown на заголовки, однако предвидел проблемы с тем, как это будет переводиться в HTML. Но дело обернулось еще лучше. Для конвертации я решил использовать популярную библиотеку markdown и в процессе изучения документации обнаружил, что в ней уже реализована возможность сбора оглавления на основе заголовков из markdown-документа. Для этого в ней есть расширение TOC. Для использования в самом документе необходимо разместить строку-маркер (по умолчанию "[TOC]") в том месте, куда необходимо вставить оглавление, и указать соответствующий аргумент при конвертации. Оглавление будем строить по заголовкам только первого уровня, здесь как раз и пригодилось смещение исходных заголовков на уровень вниз. Получаем наш конспект уже в формате HTML, а также имеем кликабельное оглавление: Теперь нужно создать PDF-файл. Для конвертации HTML в PDF использовал библиотеку pdfkit, она работает с утилитой wkhtmltopdf.  Причешем внешний вид выходного файла, используя аргумент options: Здесь столкнулся с загвоздкой в виде того, что у wkhtmltopdf по умолчанию включена "автоподстройка" контента под страницу, что с одной стороны дает из коробки правильный масштаб, но с другой на некоторых файлах может привести к неконтролируемому его изменению. Поэтому пришлось поиграться с параметрами и сделать костыль в виде отключения этой опции и вычисления значения для zoom, чтобы результат по крайней мере был предсказуемым. И поиграемся со шрифтами добавим немного оформления в CSS (размеры текста указаны с учетом выставленного ранее масштаба): Получаем на выходе вот такую красоту: И так создаем конспекты для всех программ обучения Время выполнения на 18 программах обучения - в среднем 150-170 секунд из-за генерации PDF. Небыстро, но и задачи оптимизации и ускорения мы перед собой не ставили.  Курс еще в процессе и программы обучения доступны на платформе не все, но при добавлении или изменении материалов теперь можно быстро получить актуальную версию конспекта. Исходники на Github: https://github.com/anador/e-queo-pdf upd. 31.08.2022 - вернул статью из черновиков по согласованию с представителями Эквио.