Ниже 16 практичных паттернов python-docx с короткими фрагментами кода и комментариями. Все опираются на официальную документацию python-docx 1.2.0
Идея: входные данные не должны "расползаться" по .add_paragraph().
from dataclasses import dataclass from docx import Document @dataclass(frozen=True) class ReportData: title: str rows: list[tuple[str, int]] def render_report(data: ReportData) -> Document: doc = Document() # грузит встроенный шаблон, если путь не задан doc.add_heading(data.title, level=0) # Title/Heading N # дальше - чистая "верстка" return doc
Идея: либо используете существующие стили ("Heading 1", "Body Text"), либо добавляете свои, но код почти не трогает ручные параметры форматирования.
from docx.enum.style import WD_STYLE_TYPE styles = doc.styles # Создаем стиль один раз, дальше используем имя как контракт s = styles.add_style("ReportBody", WD_STYLE_TYPE.PARAGRAPH) s.base_style = styles["Normal"] # наследование p = doc.add_paragraph("...", style="ReportBody")
Нюанс: имена встроенных стилей в файле - на английском, даже если Word локализован.
.text при сохранении run-форматирования"Paragraph.text = ... заменяет содержимое на один Run и стирает run-level форматирование. Это полезно осознанно (когда форматирование не важно), но вредно для "богатых" параграфов.
p = doc.add_paragraph() p.add_run("Итого: ").bold = True p.add_run("123 456") # форматирование останется # p.text = "..." # так делать нельзя, если нужно сохранить bold/italic в runs
Если нужно "очистить и заново собрать" параграф, используйте paragraph.clear(): он удаляет контент, но сохраняет paragraph-level форматирование.
Для читаемых отчётов часто важно: заголовок не отрывать от первого абзаца, блок держать на странице и т.п. Это делается через ParagraphFormat и tri-state свойства (True/False/None).
from docx.enum.text import WD_ALIGN_PARAGRAPH h = doc.add_paragraph("1. Раздел", style="Heading 1") h.paragraph_format.keep_with_next = True # заголовок с последующим абзацем p = doc.add_paragraph("Текст...", style="ReportBody") p.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY # выравнивание
Если часть отчёта "широкая" (таблицы), добавляйте новую секцию и меняйте параметры секции.
from docx.enum.section import WD_SECTION sec = doc.add_section(WD_SECTION.NEW_PAGE) # новая секция с разрывом # дальше на sec меняются поля/ориентация, и у неё же свой header/footer
Паттерн для "слева/по центру/справа" в колонтитулах: табы + стиль Header/Footer (tab-stops обычно зашиты в стиль).
section = doc.sections[0] hdr = section.header # всегда есть объект header p = hdr.paragraphs[0] p.text = "Лево\tЦентр\tПраво" p.style = doc.styles["Header"] # tab-stops чаще всего определены в Header
У Word таблиц есть "подводные камни": merged cells, omitted cells, вложенные таблицы. python-docx по умолчанию "приближает" таблицу к матрице (повторяет значения merged).
Практика: для "с нуля" делайте таблицы регулярными, без omitted-cells, если потом планируете читать/переиспользовать.
python-docx официально поддерживает inline pictures; плавающие (drawing layer / floating) не поддержаны как полноценная фича.
from docx.shared import Inches doc.add_picture("plot.png", width=Inches(6)) # вставка inline-пикчи в отдельном параграфе
Для отчётов (особенно корпоративных) полезно выставлять author, title, subject, created и т.д.
cp = doc.core_properties cp.author = "ACME Reporting" cp.title = "Monthly Report" cp.subject = "Finance"
Комментарий можно "якорить" на диапазон runs, но нельзя добавлять в header/footer; диапазон должен начинаться/заканчиваться на границе run.
p = doc.add_paragraph() r1 = p.add_run("Проверьте число ") r2 = p.add_run("12345") doc.add_comment(runs=[r1, r2], text="Нужно сверить с источником", author="QA") #
Для сложных отчётов (колонтитулы, логотипы, стили, номера страниц, TOC) практичнее держать всё в .docx-шаблоне и заполнять переменные. Это ровно тот кейс, под который создан python-docx-template (docxtpl): Word редактирует человек, Python только "рендерит контекст".
from docxtpl import DocxTemplate doc = DocxTemplate("template.docx") doc.render({"company_name": "World company"}) doc.save("out.docx")
Word часто режет текст на несколько runs. Для "ручной" замены в python-docx (без docxtpl) это критично: ищите/меняйте внутри runs, а плейсхолдер в шаблоне держите "цельным".
База (работает, если токен в одном run):
def replace_tokens_in_runs(paragraph, mapping: dict[str, str]) -> None: for run in paragraph.runs: for k, v in mapping.items(): token = f"{{{{{k}}}}}" if token in run.text: run.text = run.text.replace(token, v)
Почему не paragraph.text - см. паттерн 3: оно стирает run-форматирование.
Плейсхолдеры могут быть в таблицах и колонтитулах. Учитывайте, что header/footer - привязаны к секциям.
def iter_paragraphs_everywhere(doc): for p in doc.paragraphs: yield p for t in doc.tables: for row in t.rows: for cell in row.cells: yield from cell.paragraphs for sec in doc.sections: yield from sec.header.paragraphs yield from sec.footer.paragraphs
Практическое ограничение: поля Word (например, номера страниц) - это не "простой текст". Есть зафиксированные кейсы, когда обращение/присваивание paragraph.text в колонтитулах приводит к "плоскому тексту" вместо поля.
Вывод-паттерн:
footer.paragraphs[0].text = ... в параграфах с полями; редактируйте runs точечно или оставляйте эти параграфы нетронутыми.Если у вас "динамическая таблица на N строк", условные блоки, форматирование - docxtpl даёт Jinja2-подобные теги и расширения (таблицы, inline image, RichText, sub-documents).
Паттерн "гибрид":
docxtpl.render(context) - заполнение "логики шаблона";python-docx и сделать пост-обработку (например, добавить секцию/комментарии/пару служебных абзацев).Два типичных требования в отчётах:
У python-docx часть этого не покрыта "в лоб" высокоуровневым API и обычно решается либо OOXML-манипуляцией, либо (лучше) настройкой в шаблоне. Для TOC/полей - тем более.