Создание MergeField в .docx на Python

в 17:58, , рубрики: MergeField, python, python-docx

Введение

Доброго времени суток.

Сразу скажу, что я не разработчик. Лишь системный-аналитик в абстрактной международной компании. Так что, прошу за код не бить палками.

Цель статьи: если кто-то будет гуглить про встраивание MergeField в docx с помощью Python, то это заняло чуть меньше времени чем у меня.

Предыстория. Как обычно бывает, бизнес что-то придумал, собрали встречу. У юристов была/есть проблема, что пункты в различных договорах одинаковые и если надо внести изменения в одном таком пункте, то надо пересмотреть абсолютно все договора, где он участвует. Юристы показали идею, что есть excel документ, где указаны пункты договора. Эти пункты можно промаркировать, что они относятся к одному или другому документу. Потом должен появится какой-то VBA скрипт (или что-то похожее), который должен уже собрать полноценные документы по маркировкам. Формат выходного файла - docx. Стандартный шаблонный документ.

Мне задача показалась интересной, тем более уже был опыт работы на Python с библиотекой pandas для чтения данных из excel. Вторым пунктом для выбора технологии послужило, что я VBA знаю и понимаю гораздо хуже чем Python.

Так это превратилось в pet-проект.

Собственно, можно сказать, здесь началось самое интересное. Чтение файла и создание docx не вызвало особо проблем. Но, как все понимают, в договорах довольно много переменных, взять хотя бы данные клиента, которые должны быть вставлены блок подписей документа.

Про MergeField и Python

Переменные в docx реализованы через MergeField.

Создание MergeField в .docx на Python - 1

Соответственно встает первый вопрос, который вызвал проблемы - как с помощью библиотеки python-docx вставить такие поля?

Не нашел стандартной функции. Нашел исключительно примеры прямого встраивания структуры XML в документ. Мне, как далеко не разработчику и избалованного библиотеками Python, это сделало больно. Конечно же пришлось написать свою функцию. Об этом и пойдет речь далее.

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

Пример взят из: https://stackoverflow.com/questions/37518114/fill-mergefield-using-openxml

<w:r>
  <w:fldChar w:fldCharType="begin" />
</w:r>
<w:r>
  <w:instrText xml:space="preserve"> MERGEFIELD  TestFoo  * MERGEFORMAT </w:instrText>
</w:r>
<w:r>
  <w:fldChar w:fldCharType="separate" />
</w:r>
<w:r w:rsidR="00FA6E12">
  <w:rPr>
    <w:noProof />
  </w:rPr>
  <w:t>«TestFoo»</w:t>
</w:r>
<w:r>
  <w:rPr>
    <w:noProof />
  </w:rPr>
  <w:fldChar w:fldCharType="end" />
</w:r>

Соответственно тоже сделал по аналогии.

Получилось, что-то похожее, как на странице - https://github.com/python-openxml/python-docx/issues/262

def _create_field(self, paragraph, value, text="F9"):
        r = paragraph._p.add_r()
        sfldChar = OxmlElement('w:fldChar')
        sfldChar.set(qn('w:fldCharType'), "begin")
        r.append(sfldChar)
        r = paragraph._p.add_r()
        instrText = OxmlElement('w:instrText')
        instrText.text = value
        r.append(instrText)
        r = paragraph._p.add_r()
        fldChar = OxmlElement('w:fldChar')
        fldChar.set(qn('w:fldCharType'), "separate")
        r.append(fldChar)
        paragraph.add_run(text)
        r = paragraph._p.add_r()
        efldChar = OxmlElement('w:fldChar')
        efldChar.set(qn('w:fldCharType'), "end")
        r.append(efldChar)
        return DocxField(sfldChar, efldChar)

Но мне показалось, что данное решение не очень красивое и не все учитывает, что есть в Word.

Реализация

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

from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.text.run import Run


def add_mergefield(field_name: str, **kwargs) -> Run:
    '''
    Add mergefield in docx.text.run

    Usage: add_mergefield(
        'str',
        before='text',
        after='text'
    )

    :param field_name: the name of new field
    :param kwargs:
        run = run where you need to set mergfiled;
        format = one of |Upper|, |Lower|, |FirstCap|, |TitleCase|;
        before = text before field;
        after = text after field;
        mapped = True - mapped field;
        vertical = True - vertical format
    :return: Run with added mergfiled
    '''
    if 'run' in kwargs:
        run = kwargs['run']
    else:
        run = Document().add_paragraph('').add_run()._r

    field_option = ''
    field = ''

    if field_name[0:1] == '«' and field_name[-1:] == '»':
        field_option = f' MERGEFIELD ' + field_name[1:-1]
        field = field_name
    else:
        field_option = f' MERGEFIELD ' + field_name
        field = '«' + field_name + '»'

    ordered_kwargs = {}
    if 'format' in kwargs: ordered_kwargs['format'] = kwargs['format']
    if 'before' in kwargs: ordered_kwargs['before'] = kwargs['before']
    if 'after' in kwargs: ordered_kwargs['after'] = kwargs['after']
    if 'mapped' in kwargs: ordered_kwargs['mapped'] = kwargs['mapped']
    if 'vertical' in kwargs: ordered_kwargs['vertical'] = kwargs['vertical']

    for key, value in ordered_kwargs.items():
        if key == 'format':
            if value == 'Upper':
                field_option += f' * Upper'
            if value == 'Lower':
                field_option += f' * Lower'
            if value == 'FirstCap':
                field_option += f' * FirstCap'
            if value == 'TitleCase':
                field_option += f' * Caps'
        if key == 'before':
            field_option += f' \b ' + value
        if key == 'after':
            field_option += f' \f ' + value
        if key == 'mapped' and value == True:
            field_option += f' \m'
        if key == 'vertical' and value == True:
            field_option += f' \v'

    field_option += f' * MERGEFORMAT '

    ordered_kwargs = {}
    if 'before' in kwargs: ordered_kwargs['before'] = kwargs['before']
    if 'after' in kwargs: ordered_kwargs['after'] = kwargs['after']
    if 'format' in kwargs: ordered_kwargs['format'] = kwargs['format']

    for key, value in ordered_kwargs.items():
        if key == 'before':
            field = value + ' ' + field
        if key == 'after':
            field = field + ' ' + value
        if key == 'format':
            if value == 'Upper':
                field = field.upper()
            if value == 'Lower':
                field = field.lower()
            if value == 'FirstCap':
                field = field.capitalize()
            if value == 'TitleCase':
                old_field = field
                field = ''
                for str in old_field.split():
                    field += str.capitalize() + ' '
                field = field.strip()

    # <w:fldSimple w:instr=" MERGEFIELD $offerNumber * Upper b asd * MERGEFORMAT ">
    # <w:r>
    # <w:t>ASD «$OFFERNUMBER»</w:t>
    # </w:r>
    # </w:fldSimple>
    fld = create_element('w:fldSimple', run)
    create_attribute(fld, 'w:instr', field_option)
    obj = create_element('w:r', fld)
    obj = create_element('w:t', obj)
    obj.text = field

    return run

def create_element(name:str, parent=None):
    '''
    Create new object in XML tree.

    :param name: type name of new object
    :param parent: obj created by OxmlElement()
    :return: created Object OR created child Object
    '''
    sub_obj = OxmlElement(name)
    if parent is not None:
        try:
            parent.append(sub_obj)
            return sub_obj
        except Exception:
            print('oops')
    else:
        return sub_obj


def create_attribute(element, name, value):
    element.set(qn(name), value)

Вторым вопросом с которым я столкнулся, была необходимость вставлять форматированный текст из excel. Здесь я решил задачу с помощью markdown и в частности библиотеки mistletoe. Данная библиотека возвращает массив с указанием стилей.

И вишенкой на торте стала необходимость передать все это добро обычному пользователю. Здесь, решение тоже довольно стандартное. Использовалась библиотека auto-py-to-exe.

Конечный результат можно посмотреть здесь.

Заключение

Конечно же данное решение не может считаться промышленным и это чистой воды тренировка. И бизнесу оно тоже не может подойти в рамках целевого решения. Оно может применяться, как заглушка на время, и, конечно, расширяемо, и все можно доработать. И идей куча, что можно было бы сделать. Как минимум, киллер фича ворда - отображение внесенных изменений в документ. Для юристов это довольно критично и удобно. Но рамками excel это уже не решается, по крайней мере, на мой взгляд. Должна появится какая-то БД. Но это никому не нужно на текущий момент, а значит можно найти более стоящие задачи.

Надеюсь, что это кому-то помогло и что у кого-то другого ушло чуть меньше времени на гугление информации на тему "как создать MergeField в docx". 

Автор: Денис Бурцев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js