Создайте целую систему генерации PDF с помощью Amazon Web Services и Python

Несколько лет назад я участвовал в проекте, предназначенном для небольших компаний и предпринимателей. Это было типичное SaaS-приложение с регистрацией пользователей и механизмом входа в систему; пользователи могли оцифровывать продукты, заказы и продажи компании на платформе. Затем они могли визуализировать множество потрясающих диаграмм и проекций и загружать их в виде отчетов в формате PDF.

Весь проект был очень стандартным с технической точки зрения, но часть генерации отчета была чертовски интересной, я поделюсь ею в этой статье.

Требования

  • В это время я работал с масштабируемой бессерверной архитектурой на Amazon, поэтому мне нужно было заставить генерацию PDF работать на Lambda.
  • Я разработал все ядро ​​​​бизнеса (расчеты, прогнозы и т. д.) с использованием Python, поэтому я искал максимально возможное решение на основе Python.
  • Отчеты в формате PDF должны были быть похожи или иметь такую ​​же визуальную идентичность, что и контент, отображаемый на платформе: стили, цвета, шрифты и т. д.

Обходной путь

Сначала я понял, что использую библиотеку для генерации PDF, например, HTML2PDF или FPDF. Но много лет назад эти библиотеки были довольно ограничены: сгенерированные PDF-файлы были визуально плохими, а диаграммы были уродливыми.

Затем я подумал, смогу ли я создать HTML-шаблон со всеми замечательными визуальными вещами, которые мне нужны, сфотографировать его и вставить в PDF-файл. Хорошие новости: это возможно с библиотекой Puppeteer и ее Python-эквивалентом Pyppeteer.

Следуя этой идее, мы сделаем следующее: запустим экземпляр браузера на Lambda, перейдем к HTML-шаблону, заполним его пользовательскими данными, создадим PDF-файл и сохраним его в корзине S3.

Это будет окончательная архитектура нашего приложения:

В этой статье основное внимание будет уделено части Lambda и генерации PDF; Я предполагаю, что у нас уже есть пользовательские данные. Если вы хотите сделать отличную интеграцию с Cognito, API Gateway + Authorizer и базой данных RDS, я приглашаю вас прочитать мою предыдущую статью:



HTML-шаблон

Создание шаблона

Во-первых, нам нужно сгенерировать красивый HTML-шаблон. Поскольку я не дизайнер и не разработчик интерфейса, хорошей альтернативой для меня является покупка HTML-шаблона, такого как дешевый и красивый Шаблон створки.

После некоторых взломов HTML, CSS и Javascript я получил этот великолепный фиктивный шаблон без пользовательских данных:

Совет 1. Постарайтесь создать как можно более легкий шаблон, расположенный на быстром хостинге, без больших изображений или больших файлов для загрузки. Кроме того, файл PDF будет создан после вызова события загрузки, поэтому избегайте асинхронной загрузки и кэширования ресурсов.

В консоли Chrome асинхронная загрузка и ресурсы кеша отображаются после красной строки:

Совет 2. Каждая страница отчета в формате PDF может быть представлена ​​контейнером фиксированного размера. Например, если отчет в формате PDF будет иметь формат А4, контейнер будет иметь следующий стиль:

Совет 3. Создаваемые нами PDF-файлы могут иметь проблемы с искажением цвета. В этом случае необходимо форсировать точные цвета с помощью свойства print-color-adjust.

Совет 4. Избегайте анимации диаграмм. У каждой библиотеки диаграмм есть свой способ сделать это, вот как это можно сделать с библиотекой Chart.js:

Заполнение шаблона данными

Что ж, давайте теперь заполним наш шаблон данными. Есть много способов сделать это.

➡️ Метод строки запроса

Самый простой способ сделать это — отправить данные через URL-адрес в виде строки запроса. Я решил отправить большой параметр JSON с именем data и получить его благодаря интерфейсу URLSearchParams. Затем шаблон можно легко заполнить данными благодаря собственному Javascript или JQuery, если шаблон поддерживает это.

Это выглядит здорово"!

Примечание. Веб-браузеры ограничивают максимальную длину URL-адресов (2 МБ для Chrome). Если вам нужно отправить больше данных, я настоятельно рекомендую использовать метод localStorage.

➡️ Метод localStorage

localStorage позволяет хранить данные в сеансе веб-браузера. Мы будем читать данные следующим образом:

Серверная реализация

S3

S3 — это масштабируемый сервис хранения статического контента, мы будем хранить там PDF-файлы, сгенерированные Lambda.

➡️ Ведро

Создаем новый бакет (он же репозиторий).

➡️ Политика сегментов

Мы редактируем политику корзины, чтобы разрешить только чтение содержимого корзины.

Лямбда-слой

Lambda — отличный бессерверный сервис, который позволяет запускать код, позволяющий Amazon управлять ресурсами сервера за вас. Но прежде чем писать лямбда-функцию, нам нужно создать лямбда-слой со всеми необходимыми ресурсами, чтобы генерация PDF работала.

➡️ Браузер

Мы будем использовать Chrome, который является самым используемым браузером в мире. Официальной версии Chrome для AWS Lambda не существует, но мы надеемся, что в этом мире есть герои, такие как Марко Люти, которые преподнесли нам, простым смертным, прекрасный подарок: версию Chrome без головы для AWS Lambda.

Скачиваем стабильный релиз здесь и разархивируем его.

➡️ Пипетчик

Это пикантная часть: нам нужно сделать несколько хаков, чтобы заставить Pyppeteer работать на Lambda. Скачиваем последнюю доступную версию Pyppeteer на Pypi и открываем файл __init__.py.

По неизвестной причине библиотека версия вызывает сбой Pyppeteer на Lambda, поэтому мы комментируем весь соответствующий блок и заменяем его жестко заданным номером версии:

Единственный доступный для записи каталог в лямбда-функции — это каталог tmp, поэтому мы меняем его, жестко запрограммировав домашний каталог Pyppeteer:

Примечание: мы могли бы использовать параметр userDataDir в лаунчере Pyppeteer, чтобы установить правильный каталог, но таким образом мы удаляем зависимость AppDirs и другие встроенные зависимости.

➡️ Зависимости

Pyppeteer имеет следующие зависимости: pyee, tqdm и websockets. Качаем эти библиотеки и кладем в ту же папку, что и безголовый хром и Pyppeteer.

➡️ Заархивировать файл

Конфигурация, которую мы создаем, работает только с версией Python ≤ 3.7, поэтому наш слой и функция будут совместимы с Python 3.7.

Мы помещаем безголовый хром и библиотеки в каталог, содержащий следующие подкаталоги: lib > python3.7 > site-packages, и, наконец, заархивируем его.

➡️ Загрузка на слой

Большой! Наконец-то мы можем создать новый слой Lambda: мы загружаем созданный ранее zip-файл и выбираем Python 3.7 в качестве совместимой среды выполнения.

Наш слой готов!!

Лямбда-функция

➡️ Создание функции

Мы создаем новую лямбда-функцию с Python 3.7 в качестве среды выполнения.

➡️ Добавление слоя Pyppeteer в функцию

Перед кодированием мы должны добавить слой, который мы создали ранее, в функцию:

➡️ Кодирование функции 🚀

Теперь самая захватывающая часть, давайте закодируем функцию!

Примечания:

  • Мы предполагаем, что у нас уже есть значения data и user_id. Вы можете проверить мою предыдущую статью, если хотите сделать отличную интеграцию с другими сервисами AWS.
  • Мы создаем уникальное имя файла PDF, объединяем идентификатор пользователя и фактическую дату.
  • Мы используем функцию кавычки библиотеки urllib для кодирования данных, а затем можем быть частью URL.
  • Мы используем библиотеку asyncio, как указано в документации Pyppeteer.
  • Lambda хранила загруженные нами файлы в каталоге /opt, поэтому мы запускаем Headless Chromium, вызывая /opt/python/lib/python3.7/site-packages/headless-chromium благодаря опции executablePath функции запуск.
  • Мы используем функцию goto для перехода к шаблону HTML и функцию pdf для создания содержимого PDF.
  • Мы используем функцию put_object библиотеки boto3, чтобы сохранить наш файл PDF в созданной нами корзине S3.

➡️ Советы по функциям

Я дам вам несколько замечательных советов, которые вы можете использовать в асинхронной функции:

  • Если вы хотите получить сообщение, отображаемое консолью браузера, используйте консольное событие и класс ConsoleMessage,
  • Если вы видите в созданных PDF-файлах пикселизированное содержимое, используйте параметр deviceScaleFactor функции setViewport. Значение по умолчанию — 1,0.
  • Если вы хотите использовать метод localStorage, перейдите на страницу, используйте функцию оценки, чтобы вставить значение данных в localStorage и снова перейдите на страницу.
  • По умолчанию сгенерированный PDF-файл имеет буквенный формат. Вы можете изменить благодаря опции format.

➡️ Функциональная роль

Мы переходим к роли, связанной с функцией, и добавляем политику AmazonS3FullAccess, чтобы наша функция могла записывать файл PDF, созданный в ведре.

➡️ Настройки функций

Наша функция Lambda будет запускать экземпляр Chromium, поэтому это может занять несколько секунд и использовать соответствующую память.

Я обнаружил, что при выделенной памяти 2048 МБ файл PDF создается и записывается в корзину примерно за 3,5 секунды, поэтому мы меняем значение тайм-аута функции на 5 секунд.

Конечный результат

Это пример сгенерированного PDF-файла из шаблона, размещенного на https://alexandrebruffa.com/pdfreport/. В нем есть все, что нам нужно: пользовательские данные на потрясающих диаграммах, пользовательские шрифты, цвета и изображения. Он также имеет выбираемый текст и встроенные ссылки.

Если вы попробуете генерировать PDF после этой статьи, пожалуйста, покажите мне свои PDF-файлы в комментариях, мне будет приятно их увидеть!

Заключительные мысли

В этой статье показано, как создавать потрясающие PDF-файлы из HTML-шаблона на AWS Lambda. Мы узнали об AWS Lambda Layers, библиотеке Pyppeteer и некоторых курьезах, которые нашли по пути.

Во время своего исследования я нашел эту Статью для Медиума, написанную Рафаэлем Брэндом, она может быть полезна в качестве дополнения к моей статье.

Все идентификаторы и токены, показанные в этой статье, являются поддельными или просроченными, если вы попытаетесь их использовать, вы не сможете установить какие-либо соединения.

Особая благодарность Gianca Chavest за создание потрясающей иллюстрации.