Сообщить об ошибке.

Создание документов DOCX из шаблонов jinja2

Модуль python-docx-template в Python

Модуль python-docx в основном используется для создания документов MS Word, но не для их изменения. Модуль python-docx-template был создан, для легкого и элегантного создания множества подобных документов из заготовленных шаблонов .docx.

Идея состоит в том, чтобы создать нужный пример/шаблон документа с помощью Microsoft Word. Он может быть настолько сложным, насколько это необходимо: с изображениями, таблицами, колонтитулами, заголовками, в общем все, что можно сделать с Word. Затем вставить в него теги, используемые jinja2, непосредственно там где ожидаются изменения и сохранить полученный шаблон DOCX.

Теперь можно использовать модуль python-docx-template для создания любого количества подобных документов Word из созданного шаблона DOCX, изменяя "на лету" переменные контекста, которые транслируются в теги jinja2.

Установка модуля python-docx-template в виртуальное окружение.

Модуль python-docx-template размещен на PyPI, поэтому установка относительно проста.

# создаем виртуальное окружение, если нет
$ python3 -m venv .venv --prompt VirtualEnv
# активируем виртуальное окружение 
$ source .venv/bin/activate
# ставим модуль python-docx-template
(VirtualEnv):~$ python3 -m pip install -U docxtpl

Содержание:


Базовый пример использования:

Создайте новый документ DOCX, поместите туда строку {{ company_name }} и отформатируйте ее (поместите по центру, задайте шрифт, цвет и т.д.), а затем сохраните этот шаблон под названием word_tpl.docx:

from docxtpl import DocxTemplate

# определяем словарь переменных контекста,  
# которые определены в шаблоне документа DOCX
context = {}
context['company_name'] = 'Название компании.'

doc = DocxTemplate("word_tpl.docx")
# подставляем контекст в шаблон
doc.render(context)
# сохраняем и смотрим, что получилось 
doc.save("generated_docx.docx")

Синтаксис шаблона DOCX.

Так как в модуле используется пакет Jinja2, то можно использовать все теги и фильтры jinja2 внутри документа Word. Тем не менее, есть некоторые ограничения, чтобы Jinja2 работал корректно внутри документа Word:

Ограничения синтаксиса jinja2.

Обычные теги jinja2 должны использоваться только внутри одного и того же прогона одного и того же абзаца, его нельзя использовать в нескольких абзацах, строках таблицы, прогонах. Если лень управлять отдельными абзацами, строками таблицы и всем циклом с его стилем, то необходимо использовать специальный синтаксис тегов, как описано ниже.

Примечание:

Прогон - это объект Run в Microsoft Word и представляет собой последовательность символов с одинаковым стилем. Например, если создать абзац с символами одного стиля, то MS Word внутренне создаст только один "прогон" в абзаце. Если выделить текст жирным шрифтом в середине этого абзаца, то Word превратит предыдущий "прогон" в 3 разных "прогона" (обычный - жирный - обычный).

Важно: Всегда ставьте пробел после начального разделителя, используемого модулем jinja2 {{ и пробел перед конечным разделителем }}.

# избегайте такого написания
{{myvariable}}
{%if something%}

# вместо этого используйте:
{{ myvariable }}
{% if something %}

Получение переменных шаблона.

Чтобы получить недостающие переменные (например, забыли определить в словаре переменных контекста) после рендеринга документа, используйте:

tpl = DocxTemplate('your_template.docx')
tpl.render(context_dict)
set_of_variables = tpl.get_undeclared_template_variables()

Важно: чтобы получить набор всех ключей, определенных в шаблоне, этот метод можно использовать перед обработкой шаблона tpl.render().

Расширения jinja для DOCX.

Специальные теги шаблона DOCX.

Для управления абзацами, строками таблицы, столбцами таблицы, прогонами необходимо использовать специальный синтаксис:

  • для абзаца {%p jinja2_tag %},
  • для строк таблицы {%tr jinja2_tag %},
  • для колонок таблицы {%tc jinja2_tag %},
  • для прогонов {%r jinja2_tag %}.

Используя эти теги, python-docx-template размещает настоящие теги jinja2 в нужное место в исходный код xml документа. Кроме того, эти теги сообщают модулю об удалении абзаца, строки таблицы, столбца таблицы или прогона, где расположены начальный и конечный теги, и заботится о том, что находится между ними.

Важно: не используйте {%p, {%tr, {%tc или {%r дважды в одном и том же абзаце, строке, столбце или прогоне.

Например:

# Неправильное использование
{%p if display_paragraph %}Абзац №{{num}}.{%p endif %}

# Правильно
{%p if display_paragraph %}
Абзац №{{num}}.
{%p endif %}

MS Word рассматривает каждую строку как новый абзац, и следовательно теги {%p во втором случае не находятся в одном и том же абзаце.

Разделение и объединение текста в прогоне.

  • можно объединить тег jinja2 с предыдущей строкой, используя {%-,
  • можно объединить тег jinja2 со следующей строкой, используя -%}.

Текст, содержащий теги Jinja2, может быть слишком длинный и плохо читаемым.

Мой дом находится в {% if living_in_town %}городской{% else %}сельской{% endif %} местности и мне это нравится.

Можно использовать Shift+Enter, чтобы разделить текст, как показано ниже, а затем использовать {%- и -%}, чтобы docxtpl объединил все это:

Мой дом находится в
{%- if living_in_town -%}
 городской
{%- else -%}
 сельской
{%- endif -%}
 местности и мне это нравится.

Важно:

  • Используйте неразрывный пробел (Ctrl+Shift+Space), если нужен пробел в начале или конце строки.
  • Теги {%- xxx -%} должны быть в строке без дополнительного текста: не добавляйте какой-либо текст до или после этого тега.

Отображаемые переменные.

Для отображения переменных контекста, модуль jinja2 использует синтаксис двойных фигурных скобок {{ context_var }}.

Если переменная context_var является строкой, то специальные символы \n, \a, \t и \f будут переведены соответственно в новые строки, новые абзацы, табуляции и разрывы страниц соответственно.

Но если context_var является объектом RichText (модуля docxtpl), то необходимо указать, что изменяется фактический объект прогона Run. Обратите внимание на дополнительный символ r сразу после открывающих фигурных скобок:

{{r context_var }}

Важно:

  • Не используйте переменную r в шаблоне DOCX, так как конструкция шаблона {{r}} может быть интерпретировано как {{r без указания переменной. Тем не менее, можно использовать более длинное имя переменной, начинающееся с r. Например, {{render_color}} будет интерпретироваться как {{ render_color }}, а не как {{render_color}}.
  • Не используйте 2 раза {{r в одном и том же прогоне. Используйте метод RichText.add() для объединения нескольких строк и стилей на стороне кода python.

Цвет ячейки в таблице.

Есть особый случай, когда необходимо изменить цвет фона ячейки в таблице, в этом случае нужно поставить следующий тег в самом начале ячейки:

{% cellbg color_var %}

Переменная контекста color_var должна содержать шестнадцатеричный код цвета БЕЗ знака решетки.

Растягивание ячейки по нескольким столбцам в таблице.

Если нужно динамически распределить ячейку таблицы по нескольким столбцам (это полезно, когда есть таблица с динамическим количеством столбцов), то необходимо поместить следующий тег в самое начало ячейки для охвата num_var столбцов:

{% colspan num_var %}

Переменная контекста num_var должна содержать целое число для количества столбцов, которые нужно охватить. Смотрите пример dynamic_table.py.

Отображение зарезервированных символов.

Чтобы отобразить {%, %}, {{ или }}, можно использовать следующий синтаксис:

{_%, %_}, {_{ or  }_}

Объект RichText.

Когда используется тег {{ context_var }} в шаблоне DOCX, то он будет заменен строкой, содержащейся в переменной context_var. НО он сохранит стиль, заданный в шаблоне. Если необходимо добавить динамически изменяемый стиль, то нужно использовать: тег {{r context_var }} И объект RichText внутри переменной context_var. Таким образом можно изменить цвет, размер, стиль (жирный, курсив) и так далее, но лучше конечно - использовать Microsoft Word для определения собственного стиля символов. Вместо использования RichText() можно использовать его ссылку R().

Важно:

  • когда используется {{r}}, модуль удаляет текущий стиль символов из шаблона DOCX, это означает, что если не указать стиль в RichText(), то стиль вернется к стилю Microsoft Word по умолчанию. Это повлияет только на стили символов, но не на стили абзацев (MSWord управляет этими двумя типами стилей).
  • объекты RichText преобразуются в xml перед применением любого фильтра, поэтому RichText несовместим с фильтрами Jinja2. Нельзя написать в шаблоне DOCX что-то вроде {{r var | lower }}. Единственное решение - выполнять любую фильтрацию в коде Python при создании объекта RichText.

Пример работы объекта RichText:

Создайте новый документ DOCX, поместив в него строку {{r example }} и отформатируйте ее (задайте шрифт и размер), а затем сохраните этот шаблон под названием test_rich_tpl.docx:

from docxtpl import DocxTemplate, RichText

# открываем шаблон
tpl = DocxTemplate('test_rich_tpl.docx')

# создаем текст
rt = RichText()
# можно добавить стиль текста `mystyle`, 
# созданный при помощи модуля `python-docx` 
rt.add('a rich text', style='mystyle')
rt.add(' with ')
rt.add('some italic', italic=True)
rt.add(' and ')
rt.add('some violet', color='#ff00ff')
rt.add(' and ')
rt.add('some striked', strike=True)
rt.add(' and ')
rt.add('some small', size=14)
rt.add(' or ')
rt.add('big', size=60)
rt.add(' text.')
rt.add('\nYou can add an hyperlink, here to ')
rt.add('google', url_id=tpl.build_url_id('http://google.com'), color='#0018f9')
rt.add('\nEt voilà ! ')
rt.add('\n1st line')
rt.add('\n2nd line')
rt.add('\n3rd line')
rt.add('\n\aA new paragraph : <cool>\a')
rt.add('--- Разрыв страницы здесь (см. следующую страницу) ---\n\f')

for ul in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']:
    rt.add('\nUnderline : ' + ul + ' \n', underline=ul)
rt.add('\nFonts :\n', underline=True)
rt.add('Arial\n', font='Arial')
rt.add('Courier New\n', font='Courier New')
rt.add('Times New Roman\n', font='Times New Roman')
rt.add('\n\nHere some')
rt.add('superscript', superscript=True)
rt.add(' and some')
rt.add('subscript', subscript=True)

#Добавляем текст в начало объекта `rt`
rt_embedded = RichText('An example of ')
rt_embedded.add(rt)

# передаем созданный текст в переменную контекста
context = {'example': rt_embedded}

# передаем контекст в шаблон
tpl.render(context)
# сохраняем и смотрим что получилось
tpl.save('richtext.docx')

Гиперссылка с расширенным текстом.

Можно добавить гиперссылку к тексту, используя объект RichText со следующим синтаксисом:

from docxtpl import DocxTemplate, RichText
# в шаблоне создайте строку: 
# "Добавим гиперссылку на {{r google}}."
# и сохраните как `test_tpl.docx`
tpl=DocxTemplate('test_tpl.docx')
href = RichText()
href.add('google', url_id=tpl.build_url_id('http://google.com'), 
          color='#0018f9', underline='single')
# передаем созданную ссылку в переменную контекста
context = {'google': href}
# передаем контекст в шаблон
tpl.render(context)
# сохраняем и смотрим что получилось
tpl.save('test_href.docx')

Добавление одного или нескольких изображений.

Можно динамически добавлять в документ одно или несколько изображений (проверено с файлами JPEG и PNG). Для этого просто добавьте тег с контекстной переменной, например {{ img }} в шаблон DOCX, где img является экземпляром doxtpl.InlineImage:

from docxtpl import DocxTemplate, InlineImage
img = InlineImage(tpl, image_descriptor='python_logo.png', width=Mm(20), height=Mm(10))

В объекте InlineImage указывается объект шаблона tpl, путь к файлу изображения image_descriptor, ширину и/или высоту указывать не обязательно. Для указания высоты/ширины используется объект Length и его классы миллиметры (docx.shared.Мм) или точки (docx.shared.Pt).

Вложенные (вставляемые) документы DOCX.

Переменная шаблона, обозначенная как {{ context_var }} может содержать сложный и/или построенный с нуля с помощью модуля python-docx документ Word. Для этого нужно получить объект вставляемого документа из объекта шаблона методом .new_subdoc() и использовать его как объект документа python-docx. Смотрите пример tests/subdoc.py

from docxtpl import DocxTemplate
from docx.shared import Inches

tpl = DocxTemplate('templates/subdoc_tpl.docx')

# создаем вложенный документ, который затем вставим в
# `subdoc_tpl.docx` как переменную шаблона {{mysubdoc}}
sd = tpl.new_subdoc()
p = sd.add_paragraph('This is a sub-document inserted into a bigger one')
p = sd.add_paragraph('It has been ')
p.add_run('dynamically').style = 'dynamic'
p.add_run(' generated with python by using ')
p.add_run('python-docx').italic = True
p.add_run(' library')

sd.add_heading('Heading, level 1', level=1)
sd.add_paragraph('This is an Intense quote', style='IntenseQuote')

sd.add_paragraph('A picture :')
sd.add_picture('templates/python_logo.png', width=Inches(1.25))

sd.add_paragraph('A Table :')
table = sd.add_table(rows=1, cols=3)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = 'Qty'
hdr_cells[1].text = 'Id'
hdr_cells[2].text = 'Desc'
recordset = ((1, 101, 'Spam'), (2, 42, 'Eggs'), (3, 631, 'Spam,spam, eggs, and ham'))
for item in recordset:
    row_cells = table.add_row().cells
    row_cells[0].text = str(item[0])
    row_cells[1].text = str(item[1])
    row_cells[2].text = item[2]

# передаем созданный документ 
# в переменную контекста
context = {'mysubdoc': sd}

tpl.render(context)
tpl.save('output/subdoc.docx')

Начиная с версии docxtpl 0.12.0, можно объединить существующий .docx в качестве вложенного документа, для этого необходимо указать его путь при вызове метода .new_subdoc()

tpl = DocxTemplate('merge_docx_master_tpl.docx')
sd = tpl.new_subdoc('merge_docx_subdoc.docx')
context = {'mysubdoc': sd}

Экранирование служебных символов, перевод строки, новый абзац.

Когда вы используется {{ <var> }}, то модуль изменяет документ XML Word, это означает, что в тексте документа нельзя использовать символы <, > и &. Чтобы эти символы можно было использовать, необходимо их экранировать. Есть 4 способа:

  • в коде: context = {'var':R('my text')} и в шаблоне: {{r var }} (обратите внимание на r),
  • в коде: context = {'var':'my text'} и в шаблоне DOCX {{ var|e }},
  • в коде: context = {'var':escape('my text')} и в шаблоне: {{ var }}.
  • включить автоматическое экранирование при вызове метода рендеринга: tpl.render(context, autoescape=True) (по умолчанию autoescape=False)

Объект RichText() или его ссылка R() предлагают функции новой строки, нового абзаца, табуляции и разрыва страницы: для этого в тексте используйте специальные символы \n, \a, \t или \f соответственно.

Для получения дополнительной информации смотрите пример escape.py.

Другое решение, если в документ нужно включить список, то есть экранировать текст и управлять \n, \a и \f, то можно использовать класс Listing:

context = {'mylisting':Listing('the listing\nwith\nsome\nlines \a and some paragraph \a and special chars : <>&')}

А в шаблоне DOCX просто используйте {{ mylisting }}. С помощью Listing(), сохраняется текущий стиль символов (за исключением после текста, следующего после \a, когда начинается новый абзац).

Объединение ячеек таблицы.

Объединить ячейки таблицы по горизонтали двумя способами:

Объединить ячейки таблицы по вертикали внутри цикла for можно при помощи тега {% vm %} (смотрите пример vertical_merge.py):

Замена изображений в шаблоне DOCX.

Модуль python-docx-template не умеет динамически добавлять изображения в верхний/нижний колонтитулы, но может изменить их. Идея состоит в том, чтобы поместить фиктивное изображение в шаблон DOCX, обработать шаблон как обычно, а затем заменить фиктивное изображение другим. Это можно сделать для всех носителей одновременно.

Замена происходит в верхних и нижних колонтитулах и во всем теле документа.

Примечание:

  • Соотношение сторон будет таким же, как у замененного изображения.
  • Укажите имя файла, которое использовалось для вставки изображения в шаблон DOCX (только его базовое имя, а не полный путь).

Синтаксис для замены dummy_header_pic.jpg:

tpl.replace_pic('dummy_header_pic.jpg', 'header_pic_i_want.jpg')

Использование python-docx-template в командной строке.

Можно использовать модуль python-docx-template непосредственно в командной строке для создания DOCX из шаблона и файла json в качестве контекста:

$ python3 -m docxtpl -h
usage: python -m docxtpl [-h] [-o] [-q] template_path json_path output_filename

Make docx file from existing template docx and json data.

positional arguments:
  template_path    The path to the template docx file.
  json_path        The path to the json file with the data.
  output_filename  The filename to save the generated docx.

optional arguments:
  -h, --help       show this help message and exit
  -o, --overwrite  If output file already exists, overwrites without asking for confirmation
  -q, --quiet      Do not display unnecessary messages

Дополнительно смотрите пример module_execute.py.