וואלהשגריר ארה"ב בדרום אפריקה: הגבלות הוויזה הן רק צעד ראשוןPunchFour killed in Lagos-Ibadan Expressway crashBollywood HungamaSHOCKING: Gurugram youth takes inspiration from Vikrant Massey’s Pritam And Pedro character to allegedly con student of Rs. 24.60 lakhsThe Jerusalem PostCar crashes into residential building in Hadera, driver lightly injured, ramming attempt ruled outInquirer‘Thinking Pinoy’ blogger named assistant secretary at Office of CabSecUOLJustiça condena rede de supermercados em Manaus por aplicar escala 9x1 e servir comida estragadaCNN TürkİŞKUR GENÇLİK PROGRAMI BAŞVURU 2026 | İŞKUR Gençlik Programı başvuruları başladı mı, ne zaman başlayacak?Sky TG24Resident Evil, 15 cose da sapere sul nuovo film. FOTORTL BoulevardViola Davis krijgt prijs bij San Diego Film FestivalIl Fatto Quotidiano“Quando sono rimasta incinta i media si concentravano sulla mia età anagrafica ma io mi sentivo 27 anni. Non ci credevano, peggio per loro. In pensione? E chi me la da? Finché c’è l’ispirazione…”: parla Gianna NanniniDeadlineDisney Seeking Next Big K-Pop Hit After Striking Ten-Project Deal With KakaoMintTukaram Mundhe's latest action plan on medical devices: MRP vs procurement prices revealed
The Daily Newsstand · Free, Always
Wednesday, September 16, 2026

Как массово преобразовать DOCM в DOCX: готовое решение

Translate

Вот было у Вас такое, что на вход реализованному на 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. Да, это все «внутренняя кухня», сильно техническая, но с точки зрения информационной безопасности – настораживает...

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

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.