Как массово преобразовать DOCM в DOCX: готовое решение
Вот было у Вас такое, что на вход реализованному на 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. Да, это все «внутренняя кухня», сильно техническая, но с точки зрения информационной безопасности – настораживает...
Тем не менее, вышеприведенное паллиативное решение является рабочим и еще может принести пользу.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.