Наверняка не очень редко возникает задача печати HTML-документов с какого-то сервера в точности как задумано автором этого сервера. Делать это лучше всего не в надежде на браузер клиента, а на стороне сервера. А если на сервере крутится нечто на питоне (Django/Flask/тысячи их), то хорошо бы оценить во что это обойдется.

Для тестов выбирались такие библиотеки, чтобы как минимум были в виде пакетов в официальных репо RH-based дистрибутивов или же - в крайнем случае - можно было таковые собрать. И чтобы без долгих танцев с бубном.

В macOS всё ставилось с помощью homebrew и pip3, в Fedora - из стандартного репо (искл. xhtml2pdf - этого в репах нет, но при должной усидчивости за пару часов можно собрать вполне себе цивильный rpm).

Дано:

После тщательного отбора кандидатов накопилось аж 3:

  • python-pdfkit - адаптер к вызову бинарника wkhtmltopdf.

  • weasyprint - прокладка между html5lib и reportlab.

  • xhtml2pdf - примерно то же самое, что и weasyprint, но со своими тараканами особенностями. В таблице указано как "Pisa" (основной модуль).

Платформ для тестирования набралось под руками тоже 3 (все x64):

  • MacBook - Apple MacBookPro9.2 (13" mid 2012, i5-3210M (2.5GHz)), HDD, macOS 10.15 "Catalina", Python 3.9 (brew)

  • LinBook (так это назовем) - тот же самый макбук, но с Fedora 33, Python 3.9

  • DeskTop - Intel G3450 (3.4GHz), HDD, Fedora 33, Python 3.9

Документов для тестов - 3 (все - на одну страничку каждый):

  • ПД4 - квитанция на оплату налогов и сборов в Сбер (форма ПД-4сб). HTML ручной работы, максимально соответствующий стандартам. Требования к точности передачи задумки автора в печати довольно высокие.

  • Инструкция - чей-то документ с заголовком, комментариями, табличками и местом для подписи. Получен из .doc экспортом из Word 2007. HTML не так, чтобы очень тяжелый, но на тяп-ляп. То есть как оно и будет в жизни. Требования к точности - никакие.

  • Р21001 - последний листик (стр.5Б) формы Р21001 - с якорями для сканера, буквами в квадратиках и всем остальным, что мы так любим в документах для налоговой. Экспорт из Excel 2007, IE6-совместимо. Получилось 2 МБ формально правильного HTML, но совершенно фееричной разметки, то есть достаточно тяжелого для парсера-генератора. Требования к точности очень высокие.

Решение:

Код на коленке
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Benchmark of html-to-pdf converters.
(c) @justhabrauser, GPLv3.
"""

# 1. system
import os
import sys
from time import time
# 2. 3rd
from pdfkit import from_string          # https://github.com/JazzCore/python-pdfkit
from weasyprint import HTML             # https://github.com/Kozea/WeasyPrint
from xhtml2pdf.pisa import CreatePDF    # https://github.com/xhtml2pdf/xhtml2pdf

def __pdfkit(html: str) -> bytes:
    return from_string(html, False, options={'quiet': ''})

def __weasy(html: str) -> bytes:
    return HTML(string=html).write_pdf()

def __pisa(html: str) -> bytes:
    pdf = CreatePDF(html)
    if not pdf.err:
        pdf.dest.seek(0)
        return pdf.dest.read()

def main(indir: str, outdir: str, count: int) -> None:
    # 1. Load all htmls
    modules = (__pdfkit, __weasy, __pisa)
    html_list = list()      # (filename, content)[]
    dir_list = os.listdir(indir)
    dir_list.sort()
    for i, fn in enumerate(dir_list):
        fpath = os.path.join(indir, fn)
        if os.path.isfile(fpath) and fpath.endswith(".html"):
            print("Load '{}'".format(fn), file=sys.stderr)
            with open(fpath, "rt") as i_f:
                html = i_f.read()
                html_list.append(html)
                # 2. write results (and warm up)
                for j, m in enumerate(modules):
                    with open(os.path.join(outdir, "%d_%d.pdf" % (i, j)), "wb") as o_f:
                        o_f.write(m(html))
    # 2. for C times x I pages x J engines:
    print("Count\tPage\tEngine\tTime\n=====\t====\t======\t====")
    for c in range(count):                   # count
        for i, h in enumerate(html_list):    # html page
            for j, m in enumerate(modules):  # engine
                t0 = time()
                m(h)
                t1 = time()
                print("{}\t{}\t{}\t{}".format(c, i, j, t1-t0))

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print("Usage: {} <dir_with_htmls> <output_dir_for_pdfs>".format(sys.argv[0]), file=sys.stderr)
    elif not os.path.isdir(sys.argv[1]):
        print("Input '{}' is not dir or not exists.".format(sys.argv[1]), file=sys.stderr)
    elif not os.path.isdir(sys.argv[2]):
        print("Output '{}' is not dir or not exists.".format(sys.argv[2]), file=sys.stderr)
    else:
        main(sys.argv[1], sys.argv[2], 5)

Среднее время обработки каждого документа (в разрезе документов, библиотек и платформ (D=DeskTop, L=LinBook, M=MacBook)), сек.:

Lib

ПД4

Смета

Р21001

D

L

M

D

L

M

D

L

M

Pdfkit

0,36

0,44

1,49

0,36

0,44

1,23

1,3

1,9

6,9

Weasy

0,36

0,47

0,60

0,27

0,36

0,65

26,1

34,8

54,7

Pisa

0,12

0,17

0,28

0,29

0,41

0,68

20,4

27,3

42,2

Выводы: таракан без ног не слышит ©

Общий вывод - счастья нет. То есть я не смог ни одного кандидата однозначно выгнать на мороз или наградить золотой медалью. В среднем по больнице видно, что pdfkit дольше запрягает, но потом быстрее едет, но это и без тестов логично (хотя разница все-равно впечатляет). Ну а так каждый может оценить цифры, протестировать самостоятельно и сделать свои выводы. Я могу только привести свои личные впечатления:

  • pdfkit. Все-таки это не чистокровный питон и даже не обертка C-либы, что нарушает внутреннюю гармонию и бесит перфекционизм. Радует высокое качество полученного PDF, максимально точная передача задумки (реально WYSIWYG), максимальная скорость на тяжелых документах. Не радует неторопливость на мелких задачах и почти полная неуправляемость.

  • weasyprint. Бедненько - но чистенько. Всеядное, приемлемая (а иногда и неплохая) скорость, достаточно предсказуемый результат. Но без наворотов и без рекордов.

  • xhtml2pdf. Вредное. HTML должен не просто идеально соответствовать стандартам, он должен еще понравиться этой либе, иначе "инжалид дежице". Отдельно идут упражнения с кириллицей (кстати, я тестировал без этих упражнений (лениво), то есть не совсем корректно) и фееричность получаемого результата. За это там куча наворотов и в среднем хорошая скорость работы (как для питона).

Отдельно стоят вопросы управления разрывами страниц, нумерация страниц, хорошо бы ещё попробовать iText7 (но это вязать python с java, что из категории секаса переводит вопрос в категорию прона), wkhtmltopdf-static и иные окружения. Но я хотел просто быстро оценить порядок скорости на целевой лично для меня платформе (RHEL8+).