Генерация PDF из Python: как выбрать инструмент и не переделывать
Четыре подхода к созданию PDF в Python — от рендеринга HTML до сборки страницы вручную. Разбираем, что выигрываешь и чем платишь в каждом случае, и когда какой брать.

Задача звучит одинаково — «сформировать PDF», — но решается четырьмя разными способами, и выбор не в вкусе. Счёт-фактура с таблицей на 200 строк, договор с точной вёрсткой по макету дизайнера и чек на термопринтер требуют разных инструментов. Если взять не тот, вы упрётесь либо в невозможность нарисовать нужную вёрстку, либо в часы борьбы с раскладкой координат вручную. Ниже — что выбрать под задачу и цена каждого варианта.
Четыре подхода и их природа
Библиотеки для PDF делятся по тому, откуда берётся вёрстка. Это главный критерий, всё остальное вторично.
- HTML → PDF. Вы верстаете HTML и CSS, движок рендерит их в PDF. Сюда относятся WeasyPrint и обёртки над headless-браузером (Playwright, Puppeteer через subprocess).
- Высокоуровневый макет из кода. ReportLab с модулем
platypus: вы собираете документ из абзацев, таблиц и разрывов, библиотека сама раскладывает контент по страницам. - Ручное рисование по координатам. Низкоуровневый ReportLab (
canvas) или fpdf2: вы указываете, где именно поставить текст и линию. Полный контроль, ноль автоматики. - Правка готового PDF. pypdf и pdfrw не рисуют с нуля, а склеивают, накладывают водяной знак, заполняют поля формы в существующем файле.
Первый вопрос перед выбором библиотеки — не «какая быстрее», а «откуда берётся вёрстка: из HTML, из кода или из готового шаблона». Ответ отсекает три варианта из четырёх.
HTML → PDF: WeasyPrint
Если у вас уже есть шаблон на Jinja2 или готовая HTML-вёрстка, WeasyPrint (актуальная ветка 62.x на момент написания) — самый короткий путь. Вы рендерите шаблон в строку HTML и скармливаете её движку.
# WeasyPrint 62.x, Python 3.9+
from weasyprint import HTML
from jinja2 import Template
shablon = Template("""
<html><head><style>
body { font-family: sans-serif; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #333; padding: 6px; }
</style></head><body>
<h1>Счёт № {{ nomer }}</h1>
<table>
<tr><th>Товар</th><th>Цена</th></tr>
{% for p in pozicii %}
<tr><td>{{ p.name }}</td><td>{{ p.cena }}</td></tr>
{% endfor %}
</table>
</body></html>
""")
html_stroka = shablon.render(
nomer="A-101",
pozicii=[{"name": "Стул", "cena": "3 400 ₽"},
{"name": "Стол", "cena": "9 900 ₽"}],
)
HTML(string=html_stroka).write_pdf("schet.pdf")
Плата за удобство — WeasyPrint поддерживает не весь CSS. Флексбокс и грид работают ограниченно, сложные многоколоночные раскладки и часть новых свойств могут отрендериться не так, как в браузере. Уточняйте поддержку конкретного свойства в разделе «Supported features» официальной документации перед тем, как закладывать вёрстку на нём.
Высокоуровневый ReportLab (platypus)
Когда HTML не нужен, а документ — это поток абзацев и таблиц, которые должны сами переноситься по страницам, берут platypus. Вы описываете список «флоуэблов», библиотека раскладывает.
# reportlab 4.x, Python 3.9+
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Table, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
stili = getSampleStyleSheet()
doc = SimpleDocTemplate("otchet.pdf", pagesize=A4)
dannye = [["Товар", "Цена"],
["Стул", "3 400"],
["Стол", "9 900"]]
tablica = Table(dannye)
elementy = [Paragraph("Отчёт за месяц", stili["Title"]), tablica]
doc.build(elementy)
Цена — многословность. Стилизация таблицы задаётся отдельными командами через TableStyle, и то, что в CSS одна строка, здесь три-четыре вызова. Зато многостраничные таблицы с повторяющимися шапками и точное управление разрывами работают предсказуемо — в этом ReportLab сильнее HTML-движков.
Ручное рисование: canvas или fpdf2
Для чеков, наклеек, бейджей и всего, где макет фиксированный и мелкий, автоматическая раскладка мешает. Здесь рисуют по координатам.
# reportlab 4.x, canvas — координаты от нижнего левого угла
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
c = canvas.Canvas("chek.pdf", pagesize=A4)
c.setFont("Helvetica", 12)
c.drawString(50, 800, "Магазин на углу")
c.line(50, 795, 300, 795)
c.drawString(50, 770, "Итого: 340 ₽")
c.save()
Ловушка новичка: в ReportLab начало координат — нижний левый угол, ось Y растёт вверх. Привычка «сверху вниз» из вёрстки здесь ломается. fpdf2 (актуальная ветка 2.x) считает от верхнего левого угла и часть находит эту модель удобнее. Плата за контроль общая: перенос строк, разрывы страниц и выравнивание таблиц вы делаете руками.
Правка готового: pypdf
Если базовый PDF уже сформирован дизайнером или другой системой, а вам нужно наложить водяной знак, склеить файлы или заполнить поля формы — генерировать заново незачем. Это делает pypdf (актуальная ветка на момент написания — 4.x; старое имя PyPDF2 объединили именно в pypdf, ставьте новый пакет).
Сравнение подходов
| Подход | Откуда вёрстка | Сильная сторона | Плата |
|---|---|---|---|
| WeasyPrint | HTML + CSS | Быстро при готовом шаблоне, знакомый CSS | Поддержка CSS неполная, зависит от системных библиотек |
| ReportLab platypus | Код (флоуэблы) | Многостраничные таблицы, точные разрывы | Многословная стилизация |
| canvas / fpdf2 | Координаты | Полный контроль, лёгкий рантайм | Перенос и раскладка руками |
| pypdf | Готовый PDF | Склейка, водяные знаки, формы | Не рисует контент с нуля |
Когда что выбирать
- Есть HTML-шаблон или дизайн описан в CSS — WeasyPrint. Особенно если тот же шаблон рендерится и в веб.
- Документ — поток данных с длинными таблицами (реестры, отчёты, накладные на много страниц) — ReportLab platypus.
- Фиксированный мелкий макет (чек, наклейка, пропуск) — canvas или fpdf2.
- Изменяете чужой готовый PDF — pypdf, без генерации.
Что ломается на практике
Три ошибки встречаются чаще всего, и все три — на кириллице и шрифтах.
- Кириллица становится квадратами или пропадает. В canvas и fpdf2 встроенные шрифты (Helvetica) не содержат русских глифов. Нужно зарегистрировать TTF-шрифт с кириллицей: в ReportLab через
pdfmetrics.registerFont(TTFont(...)), в fpdf2 черезadd_fontс указанием пути к .ttf. Уточняйте актуальный синтаксис регистрации в документации своей версии. - WeasyPrint падает при импорте или не находит библиотеки. Движок опирается на системные Pango, cairo и GDK-PixBuf — на чистом сервере их нужно поставить отдельно. Ошибка вида «cannot load library» означает нехватку системной зависимости, а не проблему Python-кода.
- Таблица «уезжает» за край страницы. В platypus у Table нет автопереноса ширины по контенту так, как в HTML. Ширины колонок задавайте явно параметром
colWidths, иначе длинная ячейка вылезет за поле.
Что делать дальше
Определитесь с подходом по таблице выше и закройте вопрос шрифтов до того, как писать логику: заведите один TTF с кириллицей и зарегистрируйте его в самом начале. Дальше смотрите на два показателя в проде: время генерации одного документа (WeasyPrint с headless-рендером заметно медленнее ReportLab на объёме) и потребление памяти при пакетной генерации сотен файлов — здесь тяжёлые HTML-движки способны съесть больше, чем ожидаете. Инвалидация кеша шрифтов и параллельная генерация — отдельные темы; здесь мы разобрали только выбор инструмента.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: WeasyPrint — официальная документация, ReportLab — документация
Частые вопросы
У меня установлен PyPDF2, а не pypdf — это то же самое?
Почему кириллица выводится квадратами или вообще исчезает?
WeasyPrint не импортируется на сервере, хотя pip его поставил. Что не так?
Мне нужен точный макет по PDF от дизайнера, но с подставляемыми данными. Какой подход?
Можно ли отрендерить сложный CSS-грид через WeasyPrint?
Что выбрать, если документов сотни в час и важна скорость?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.