Вот было у Вас такое, что на вход реализованному на 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. Не вайбкодинг!

