HTML ⟹ PDF @ Python

Наверняка не очень редко возникает задача печати 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+).

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 6 322 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 7

    0
    Очень интересует вопрос оверлеев в pdf. Наложить номера страниц на готовый, добавить хдер-футер. Есть pdftk, но и как-то больше ничего не попадалось на глаза
      0

      Легенды гласят, что это умеет PyPDF2/PyPDF4 и другие библиотеки операций с сырыми PDF (да хоть и тот же reportlab, если сильно прижмет).
      Это если речь о питоне, а не яве (pdftk и иже — это к ним).

        0
        Натыкались на PyPDF2, не обнаружили кастомных оверлеев. Правда и искали минут 5-10, так как посчитали что проект умер ввиду отсутствия коммитов несколько лет. А вот PyPDF4 не видели, спасибо.

        Попадался еще pyfpdf. Пробовать не доводилось?
        0
        wkhtmltopdf умеет ставить страницы, хедер-футеры и т.п.
        Нужно быть внимательным, разные версии немного по-разному рендерят pdf(масштаб на страницу, отступы).
        Из подводных камней мы сталкивались не с временем рендеринга, а с тем, что некоторые верстки с js генерятся в документ, который дико долго открывается и тормозит.
          0
          Обратите внимание, я спрашивал о другом. wkhtmltopdf используется для генерации большого числа разрозненных PDF, которые в последующем успешно склеиваются. Так что исходный материал в данном случае — PDF.

          Не видел именно у этой тулзы функций работы с готовым PDF.
        0
        В своё время решал примерно похожую задачу (один раз заPDFить большую кучу страниц с сайта в один файл) с помощью Puppeteer. С помощью инжекта CSS Print Styles правилась вёрстка в печатный вид, браузер печатал каждую страницу в отдельный PDF файл и в конце склеивался easy-pdf-merge.
          +1
          Я рад что вспомнили reportlab. В тесте для этой статьи, скорее всего, он стоял бы ниже Html2Pdf с пометкой «очень вредное». Для старта не порекомендую и врагу. А вот потом…

          А вот это потом: Я работаю уже 7 год с reportlab. В 2016 смотрел Html2Pdf, в 2017 смотрел на Weasy, и всегда возвращался обратно. На тот момент reportLab никто не мог переплюнуть по возможностям. Когда-то мне были нужны автосборки индексов и оглавлений, когда-то нестандартная нумерация страниц и печать с пометками линий реза для типографии.

          Сложности:
          Не все шрифты можно было импортировать, пришлось допилить.
          Кернинг не работает нормально. Пришлось разбивать слова на символы и управлять шириной пробела.
          Подчеркивание слов всегда неразрывной линией, не стал менять.
          Падает, если в HTML есть тег span с классом или стилями. Поставил чистилку.

          На текущий момент у нас написано одних только reportLab «улучшалок» на 11 тыс строк. И мне нравится как выглядит конечный продукт, у нас это PDF каталоги продукции.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое