Вот было у Вас такое, что на вход реализованному на Python автоматизированному процессу, принципиально не использующему приложение Microsoft Word и COM‑объекты, нет‑нет да и придет файл с расширением docm? Ведь фактически, чтобы обращаться к текстовым файлам, на текущий момент в экосистеме Python существует только одна более‑менее функциональная (и, что греха таить, бесплатная) библиотека — python‑docx. А она позволяет работать только с docx‑файлами. Можно, конечно, задействовать Word и выполнить преобразование путем пересохранения, но мы же помним — принципы (прошу не считать это глупостью, просто если можно работать с файлами напрямую — лучше так и делать). Можно взять другое приложение, взаимодействуя с ним посредством GUI через использование pywinauto и pyautogui, но здесь получается все не всегда просто и не быстро: заменили приложение, у приложения изменился интерфейс — начинай сначала…

Заказчику, меж тем, нет дела до наших трудностей: его файл(ы) автоматизированный процесс должен уметь обрабатывать. К счастью, doc‑файлы уже не в ходу, иначе бы дальше говорить было не о чем.

Так вот, памятуя о том, что и формат docx, и формат docm — суть архивы xml‑файлов (да, там есть еще некие.rels, но внутри они — те же xml’ки), мы задались вопросом: а что если преобразование одного формата в другой реализовать на файловом уровне? Вот прям забраться внутрь и внести необходимые изменения?

Сказано — сделано! Вот скрипт:

import os
import zipfile
import re


def docm2docx_convert(input_file: str, output_file: str, info=False) -> bool:
    """
    Преобразование docm-файла в docx (файловая обработка, приложения не используются)
    
    Параметры:
        input_file: str - полный путь к преобразуемому docm-файлу
        output_file: str - полный путь к преобразованному docx-файлу
        info: bool - флаг вывода в консоль информации о результатах преобразования
    """

    if str(input_file).lower()[-4:] != 'docm':
        if info:
            print(f'{os.path.basename(input_file)} - это не docm-файл, преобразование невозможно')
        return False

    # распаковка zip-архива, каковым является docm-файл
    input_zip = zipfile.ZipFile(input_file)
    all_content_dct = {name: input_zip.read(name) for name in input_zip.namelist()}
    
    # удаление тех xml-файлов, которые не нужны в docx-файле
    files2del_lst = ['vbaData.xml', 'vbaProject.bin', 'vbaProject.bin.rels']
    for xml_file_as_key in list(all_content_dct.keys()):
        if [file2del for file2del in files2del_lst if file2del in xml_file_as_key]:
            del all_content_dct[xml_file_as_key]

    for xml_file_as_key, xml_content_as_value in all_content_dct.items():

        # обработка '[Content_Types].xml'
        if xml_file_as_key == '[Content_Types].xml':
            xml_content_as_str = xml_content_as_value.decode(encoding='utf-8')
            xml_content_as_lst = re.findall(r'<.+?>', xml_content_as_str)

            # удаление ненужных элементов
            for xml_element in reversed(xml_content_as_lst):
                if [el2del for el2del in (
                        'vnd.ms-office.vbaProject', 'vnd.ms-word.vbaData+xml',
                        ) if el2del in xml_element]:
                    xml_content_as_lst.remove(xml_element)

            # изменение типа документа
            for i, xml_element in enumerate(xml_content_as_lst):
                if 'vnd.ms-word.document.macroEnabled.main+xml' in xml_element:
                    xml_element_with_repl = xml_element.replace(
                        'vnd.ms-word.document.macroEnabled', 
                        'vnd.openxmlformats-officedocument.wordprocessingml.document'
                        )
                    xml_content_as_lst[i] = xml_element_with_repl

            # добавление элементов
            checked_element = '<Default Extension="emf" ContentType="image/x-emf"/>'
            if checked_element not in xml_content_as_lst:
                xml_content_as_lst.insert(2, checked_element)
            checked_element = ('<Override PartName="/word/stylesWithEffects.xml" '
                               'ContentType="application/vnd.ms-word.stylesWithEffects+xml"/>')
            if checked_element not in xml_content_as_lst:
                xml_content_as_lst.insert(-1, checked_element)

            all_content_dct[xml_file_as_key] = ''.join(xml_content_as_lst).encode(encoding='utf-8')

        # обработка 'word/_rels/document.xml.rels'
        if xml_file_as_key == 'word/_rels/document.xml.rels':
            xml_content_as_str = xml_content_as_value.decode(encoding='utf-8')
            xml_content_as_lst = re.findall(r'<.+?>', xml_content_as_str)

            # удаление ненужного элемента
            for xml_element in reversed(xml_content_as_lst):
                if 'Target="vbaProject.bin"' in xml_element:
                    xml_content_as_lst.remove(xml_element)

            # обновление нумерации идентификаторов
            r_id = 1
            for i, xml_element in enumerate(xml_content_as_lst):
                r_id_search = re.findall(r'Id="rId\d"', xml_element)
                if r_id_search:
                    xml_element_with_repl = xml_element.replace(r_id_search[0], f'Id="rId{r_id}"')
                    xml_content_as_lst[i] = xml_element_with_repl
                    r_id += 1

            all_content_dct[xml_file_as_key] = ''.join(xml_content_as_lst).encode(encoding='utf-8')

    # упаковка архива в docx-файл
    with zipfile.ZipFile(output_file, mode='w') as conv_file:
        for xml_filename, xml_content in all_content_dct.items():
            conv_file.writestr(zinfo_or_arcname=xml_filename, data=xml_content, compress_type=zipfile.ZIP_DEFLATED)
   
    if info:
        print(f'Файл "{os.path.basename(input_file)}" преобразован в "{os.path.basename(output_file)}"')
    return True

Следует отметить, что макросы, которые могли пробраться в docm‑файлы заказчика, мы в своем автоматизированном процессе не используем. Равно как и заказчику они уже больше не нужны. Другими словами, все макросодержимое можно было выпиливать на корню, что значительно упрощало задачу.

Docm‑файлы для проверки создавались в Microsoft Office 2007, Microsoft Office 2010 и Microsoft Office 2016, а также в LibreOffice 7.5.3.2 и LibreOffice 26.8.0.3 (ну что было под рукой). Версия модуля python‑docx, который потом успешно прочитал полученные docx‑файлы: 1.1.2.

Интересно, что LibreOffice VBA‑макросы внутри *.docm, не сохраняет, но файл с таким расширением создает успешно. Поэтому надежды на импортозамещение, которое сотрет с наших компьютеров все, что относится к Microsoft, и окончательно остановит поток файлов с макросами, слегка поблекли.

Несколько моментов в копилку с каждым днем становящихся все более бесполезными знаний, дополнительно поясняющих код:

1. Модуль python‑docx перед открытием проверяет не расширение файла (поскольку просто переименованный в docm “чистый” docx он без проблем читает, хотя Word “ругается”), а вот этот xml‑элемент в [Content_Types].xml:

<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>

В docm‑файле этот элемент выглядит так:

<Override PartName="/word/document.xml" ContentType="application/vnd.ms-word.document.macroEnabled.main+xml"/>

Если не выполнить замену второго на первый, то даже без макросов docx‑файл модулем python‑docx никак не прочитать.

2. Файл с макросом, созданный в Microsoft Office 2007, в том же [Content_Types].xml не содержит следующий элемент:

<Default Extension="emf" ContentType="image/x-emf"/>

Модуль python‑docx без этого отказывается читать docx, даже если тот клянется Матрицей, что не содержит макросы. Интернет посоветовал включить «заглушку»:

<Override PartName="/docProps/thumbnail.emf" ContentType="application/octet-stream"/>

Но потом оказалось, что 2010-й и 2016 офисные пакеты добавляют свой элемент, который конфликтует с вышеуказанным:

<Default Extension="emf" ContentType="image/x-emf"/>

В общем, наличие этого элемента необходимо проверять, если его нет — добавлять.

3. Архив xml’ей, создаваемый Word в 2007-м и 2016-м «офисах» содержит структуру из шести файлов: settings.xml, stylesWithEffects.xml, styles.xml, theme1.xml, fontTable.xml, webSettings.xml, а вот 2010-й пакет добавляет еще два файла: endnotes.xml и footnotes.xml. Да, это все «внутренняя кухня», сильно техническая, но с точки зрения информационной безопасности — настораживает...

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

P. S. Не вайбкодинг!