Сообщить об ошибке.

Практичные паттерны для модуля python-docx

Содержание:

Ниже 16 практичных паттернов python-docx с короткими фрагментами кода и комментариями. Все опираются на официальную документацию python-docx 1.2.0

A. Генерация отчётов "с нуля" (python-docx как рендерер)

Document-as-Renderer: доменная модель отдельно, Word-верстка отдельно

Идея: входные данные не должны "расползаться" по .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

Style-first: стили как "контракт" между Word и кодом

Идея: либо используете существующие стили ("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 форматирование.

Pagination-control для отчётов: keep_with_next / page_break_before

Для читаемых отчётов часто важно: заголовок не отрывать от первого абзаца, блок держать на странице и т.п. Это делается через 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 # выравнивание

Sections как способ менять ориентацию/поля/колонтитулы блоками

Если часть отчёта "широкая" (таблицы), добавляйте новую секцию и меняйте параметры секции.

from docx.enum.section import WD_SECTION

sec = doc.add_section(WD_SECTION.NEW_PAGE) # новая секция с разрывом
# дальше на sec меняются поля/ориентация, и у неё же свой header/footer

Header/Footer как "story": редактируются как Document, плюс "зоны" табами

Паттерн для "слева/по центру/справа" в колонтитулах: табы + стиль 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

Table-builder: писать таблицы "по слоям", помнить про merged/omitted/recursive

У Word таблиц есть "подводные камни": merged cells, omitted cells, вложенные таблицы. python-docx по умолчанию "приближает" таблицу к матрице (повторяет значения merged).

Практика: для "с нуля" делайте таблицы регулярными, без omitted-cells, если потом планируете читать/переиспользовать.

Inline images only: картинки как "большой символ"

python-docx официально поддерживает inline pictures; плавающие (drawing layer / floating) не поддержаны как полноценная фича.

from docx.shared import Inches

doc.add_picture("plot.png", width=Inches(6)) # вставка inline-пикчи в отдельном параграфе

Core properties: метаданные документа задавайте явно

Для отчётов (особенно корпоративных) полезно выставлять author, title, subject, created и т.д.

cp = doc.core_properties
cp.author = "ACME Reporting"
cp.title = "Monthly Report"
cp.subject = "Finance"

Комментарии: как инструмент ревью (но не в header/footer)

Комментарий можно "якорить" на диапазон 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") #

B. Заполнение шаблонов (python-docx + ограничения Word run’ов)

Template-first: "шаблон - источник истины"

Для сложных отчётов (колонтитулы, логотипы, стили, номера страниц, 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")

Placeholder-run discipline: плейсхолдер должен жить в одном run

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-форматирование.

Полный обход документа: body + таблицы + header/footer (если вы их трогаете)

Плейсхолдеры могут быть в таблицах и колонтитулах. Учитывайте, что 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

Поля (PAGE/NUMPAGES/TOC): оставляйте их в шаблоне и избегайте "опасных" операций

Практическое ограничение: поля Word (например, номера страниц) - это не "простой текст". Есть зафиксированные кейсы, когда обращение/присваивание paragraph.text в колонтитулах приводит к "плоскому тексту" вместо поля.

Вывод-паттерн:

  • поля вставляйте в Word вручную в шаблон;
  • при заполнении шаблона не делайте footer.paragraphs[0].text = ... в параграфах с полями; редактируйте runs точечно или оставляйте эти параграфы нетронутыми.
    • TOC как "фича генерации" тоже исторически запрошена отдельно.

docxtpl для циклов/таблиц/inline images/богатого текста

Если у вас "динамическая таблица на N строк", условные блоки, форматирование - docxtpl даёт Jinja2-подобные теги и расширения (таблицы, inline image, RichText, sub-documents).

Паттерн "гибрид":

  1. docxtpl.render(context) - заполнение "логики шаблона";
  2. затем открыть результат через python-docx и сделать пост-обработку (например, добавить секцию/комментарии/пару служебных абзацев).

"Ограничения high-level API" и безопасный выход через шаблон

Два типичных требования в отчётах:

  • повторять строку заголовка таблицы на каждой странице;
  • сложные поля/TOC/номер страницы.

У python-docx часть этого не покрыта "в лоб" высокоуровневым API и обычно решается либо OOXML-манипуляцией, либо (лучше) настройкой в шаблоне. Для TOC/полей - тем более.