Задача «делать документы по шаблону из таблицы» решается бесплатно. Не «почти», не «в урезанном виде» — полностью. Ниже два рабочих способа с полным кодом, который можно скопировать и запустить сегодня.
И вторая половина статьи — о том, во что бесплатное обходится на самом деле. Не для того, чтобы отговорить: если ваш случай простой, берите код и не читайте дальше.
Оба примера работают с одинаковой парой файлов, чтобы их можно было сравнить честно.
Таблица Данные.xlsx — первая строка заголовки, дальше строки данных. Колонки: ФИО, Должность, Дата, Сумма, Имя файла. Последняя служебная — из неё берётся имя готового документа.
Шаблон Шаблон.docx — обычный документ Word, в котором на месте переменных стоят метки в двойных фигурных скобках: {{ФИО}}, {{Должность}}, {{Дата}}, {{Сумма}}. Имена меток совпадают с заголовками колонок.
Оба файла лежат в одной папке. Результат складывается в подпапку.
Один момент отмечу заранее, потому что он определяет всё дальнейшее: Word имеет привычку незаметно разрывать текст на фрагменты. Метка, которую вы видите как {{ФИО}}, внутри файла может храниться как три куска: {{, ФИ, О}}. Происходит это после правок, проверки орфографии, смены языка. Визуально не проявляется никак. Про разметку шаблона подробнее — в отдельной статье.
Живёт внутри книги Excel, ничего дополнительно ставить не нужно. Управляет Word через автоматизацию — то есть Word должен быть установлен.
Как запустить: открыть книгу, нажать Alt+F11, вставить модуль (Insert → Module), вставить код, запустить F5. Книгу сохранить в формате .xlsm — обычный .xlsx макросы не хранит.
Option Explicit
Sub СоздатьДокументы()
Dim ws As Worksheet, wd As Object, doc As Object
Dim lastRow As Long, lastCol As Long, i As Long, c As Long
Dim tplPath As String, outDir As String
Dim key As String, val As String
Dim nameCol As Long, fileName As String
Set ws = ThisWorkbook.Worksheets(1)
tplPath = ThisWorkbook.Path & "\Шаблон.docx"
outDir = ThisWorkbook.Path & "\Готовые"
If Dir(outDir, vbDirectory) = "" Then MkDir outDir
lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
lastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
' служебная колонка с именем файла
nameCol = 0
For c = 1 To lastCol
If LCase(Trim(ws.Cells(1, c).Value)) = "имя файла" Then nameCol = c
Next c
Set wd = CreateObject("Word.Application")
wd.Visible = False
wd.DisplayAlerts = 0
For i = 2 To lastRow
Set doc = wd.Documents.Add(tplPath) ' новый документ на основе шаблона
For c = 1 To lastCol
key = "{{" & Trim(ws.Cells(1, c).Value) & "}}"
val = ws.Cells(i, c).Text ' .Text, а не .Value — сохраняет формат ячейки
With doc.Content.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = key
.Replacement.Text = val
.Forward = True
.Wrap = 1 ' wdFindContinue
.MatchCase = False
.Execute Replace:=2 ' wdReplaceAll
End With
Next c
If nameCol > 0 Then
fileName = ЧистоеИмя(ws.Cells(i, nameCol).Text)
Else
fileName = "Документ"
End If
doc.SaveAs2 outDir & "\" & fileName & " row" & (i - 1) & ".docx", 16
doc.Close False
Next i
wd.Quit
Set wd = Nothing
MsgBox "Готово. Создано документов: " & (lastRow - 1), vbInformation
End Sub
Private Function ЧистоеИмя(ByVal s As String) As String
Dim bad As Variant, k As Long
bad = Array("<", ">", ":", """", "/", "\", "|", "?", "*")
For k = LBound(bad) To UBound(bad)
s = Replace(s, bad(k), "_")
Next k
ЧистоеИмя = Trim(s)
End FunctionДва места в этом коде стоят отдельного внимания.
doc.Content.Find справляется с разорванными метками. Это не случайность: поиск Word работает по тексту документа, а не по внутренним фрагментам, поэтому разбитая на куски метка находится и заменяется. Большое преимущество способа, о котором обычно не пишут.
.Text вместо .Value. Свойство .Value вернёт сырое число — 45000. Свойство .Text вернёт то, что видно в ячейке с учётом её формата — 45 000,00. Для сумм и дат это решает вопрос форматирования целиком.
Ограничения: нужен установленный Word, работает только на Windows, каждый документ открывается через настоящий Word — то есть скорость измеряется секундами на документ, а не миллисекундами. И макросы во многих организациях заблокированы политиками безопасности, а файл .xlsm почтовые фильтры любят не пропускать.
Word не нужен вовсе — библиотека работает с файлом .docx напрямую. Значит, код одинаково идёт на Windows, macOS и Linux и на сервере тоже.
Установка: pip install python-docx openpyxl
Сначала — как делать не надо, потому что именно этот вариант расходится по интернету:
for p in doc.paragraphs:
for r in p.runs: # <- здесь и ломается
for k, v in data.items():
r.text = r.text.replace("{{%s}}" % k, v)Код выглядит логично и на простом шаблоне работает. Но как только Word разорвал метку на фрагменты, ни один фрагмент целиком её не содержит — и замена не срабатывает. В документе остаётся {{ФИО}} открытым текстом. Проверять это на трёх строках бесполезно: пока шаблон не правили, метки целые.
Рабочий вариант заменяет текст на уровне абзаца:
# -*- coding: utf-8 -*-
# Генерация документов Word по шаблону из таблицы Excel.
# Требуется: pip install python-docx openpyxl
import os, re, datetime
from openpyxl import load_workbook
from docx import Document
BASE = os.path.dirname(os.path.abspath(__file__))
TABLE = os.path.join(BASE, "Данные.xlsx")
TPL = os.path.join(BASE, "Шаблон.docx")
OUT = os.path.join(BASE, "Готовые")
BAD = r'<>:"/\|?*'
def fmt(v):
"""Значение ячейки -> строка. Числа с копейками — с разделителем разрядов."""
if v is None:
return ""
if isinstance(v, datetime.datetime):
return v.strftime("%d.%m.%Y")
if isinstance(v, float):
s = f"{v:,.2f}".replace(",", "\u00a0").replace(".", ",")
return s
if isinstance(v, int):
return f"{v:,}".replace(",", "\u00a0")
return str(v)
def safe(name):
for ch in BAD:
name = name.replace(ch, "_")
return name.strip() or "Документ"
def replace_in_paragraph(p, data):
"""Метка может быть разорвана Word'ом на несколько run'ов, поэтому
заменяем по тексту всего абзаца, а не по каждому run отдельно."""
full = "".join(r.text for r in p.runs)
if "{{" not in full:
return
new = re.sub(r"\{\{([^{}]+)\}\}",
lambda m: data.get(m.group(1).strip(), m.group(0)), full)
if new == full:
return
p.runs[0].text = new # форматирование берётся от первого run'а
for r in p.runs[1:]:
r.text = ""
def replace_everywhere(doc, data):
for p in doc.paragraphs:
replace_in_paragraph(p, data)
for t in doc.tables:
for row in t.rows:
for cell in row.cells:
for p in cell.paragraphs:
replace_in_paragraph(p, data)
for s in doc.sections: # колонтитулы
for part in (s.header, s.footer, s.first_page_header, s.first_page_footer):
if part is None:
continue
for p in part.paragraphs:
replace_in_paragraph(p, data)
def main():
os.makedirs(OUT, exist_ok=True)
ws = load_workbook(TABLE, data_only=True).active
headers = [str(c.value).strip() if c.value is not None else "" for c in ws[1]]
made = 0
for i, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=1):
if all(v is None for v in row):
continue
data = {h: fmt(v) for h, v in zip(headers, row) if h}
doc = Document(TPL)
replace_everywhere(doc, data)
name = safe(data.get("Имя файла", "")) or "Документ"
doc.save(os.path.join(OUT, f"{name} row{i}.docx"))
made += 1
print(f"создано документов: {made}, папка: {OUT}")
if __name__ == "__main__":
main()Последовательность в листинге — это неразрывный пробел: в русской типографике разряды в числах разделяются именно им, иначе «45 000,00» может переломиться посередине строки. Обычный пробел тоже сработает, но выглядеть будет хуже.
Что тут важно понимать.
Форматирование чисел приходится делать самому. Библиотека чтения Excel отдаёт сырое значение — 45000, а не 45 000,00. Формат ячейки, который вы настроили в таблице, до документа не доедет: функция fmt в примере воспроизводит его вручную. Это ровно та работа, которой в первом способе не было.
Замена по абзацу теряет форматирование внутри абзаца. Если в одном предложении часть слов жирная, после замены весь абзац примет оформление первого фрагмента. Для типовых документов это незаметно, для сложной вёрстки — придётся усложнять код.
Про скорость: сто документов по этому шаблону формируются за 7,4 секунды — около 70 миллисекунд на документ. Word при этом не запускается вовсе.
Третья категория — готовые бесплатные надстройки и утилиты. Конкретных названий здесь называть не буду: они появляются и исчезают, а рекомендовать то, что я не проверял на ваших данных, нечестно. Полезнее список того, что стоит выяснить до установки.
Код выше действительно бесплатный. Платите вы временем, и его стоит посчитать честно — в обе стороны.
| Этап | Первый раз | Дальше |
|---|---|---|
| Разобраться, вставить, запустить | 1–2 часа | — |
| Довести под свой шаблон и колонки | 1–3 часа | ~0,5 часа на новый вид документа |
| Поймать разорванные метки и формат чисел | 1–2 часа | — |
| Добавить картинки в документы | от 4 часов | — |
| Починить после смены шаблона | — | 0,5–2 часа за раз |
| Передать коллеге, когда вы в отпуске | — | отдельная история |
Итого на старте обычно выходит рабочий день. Цифры не универсальные — они зависят от того, насколько вам привычен код, — но порядок именно такой.
Дальше начинается то, о чём в момент «сейчас за вечер напишу» не думают.
Поддержка при смене шаблона. Юрист поменял формулировку и добавил поле — надо править код. Не сложно, но каждый раз это вы, и в тот момент, когда некогда.
Автобусный фактор. Скрипт написал один человек. Он в отпуске, документы нужны сегодня. Разбираться в чужом коде ради тридцати справок — не то, чем хочет заниматься кадровик.
Пограничные случаи. Пустая ячейка, объединённые ячейки, дата, введённая текстом, лишний пробел в заголовке колонки. Каждый вылезает по одному и каждый съедает время.
Картинки. Вставить фотографию в нужное место документа средствами python-docx можно, но это заметно больше кода, чем замена текста: нужно найти абзац, задать размеры, разобраться с ориентацией снимков с телефона. Это тот рубеж, на котором «за вечер» превращается в «за неделю».
Совершенно серьёзно, без подводки к продаже.
Считать надо не «программа против нуля», а «программа против часов».
Возьмите стоимость часа того, кто будет писать и поддерживать код. Умножьте на восемь часов первичной настройки. Прибавьте по два часа в квартал на поддержку. Сравните с ценой готового инструмента за тот же период.
Для сотрудника со стоимостью часа в тысячу рублей первичная настройка обходится примерно в восемь тысяч, а год поддержки — ещё в восемь. Это порядок, а не точный расчёт: подставьте свои цифры.
Признаки того, что расчёт уже не в пользу кода:
Если ничего из списка нет — берите код выше, он рабочий. Если есть половина — посчитайте.
Word хранит текст абзаца фрагментами и после правок может разорвать метку на части: «{{», «ФИ», «О}}». Поиск Word работает по тексту документа целиком, поэтому находит разорванную метку. Наивный код на python-docx перебирает фрагменты по одному, и ни в одном из них метки целиком нет. Лечится заменой по тексту всего абзаца — так сделано в рабочем примере.
Потому что из ячейки берётся сырое значение, а не то, что вы видите. В VBA разница в свойстве: .Value даёт число, .Text — текст с учётом формата ячейки. В Python библиотека чтения Excel отдаёт сырое значение всегда, и форматирование приходится воспроизводить кодом.
Для макроса VBA — да, он управляет настоящим Word и работает только на Windows. Для Python с библиотекой python-docx — нет: она работает с файлом .docx напрямую, поэтому код одинаково идёт на Windows, macOS, Linux и на сервере.
Если код вам привычен — рабочий день на первичную настройку под свои шаблоны и колонки, включая отладку пограничных случаев вроде пустых ячеек и разорванных меток. Дальше по полчаса на каждый новый вид документа и от получаса до двух часов при каждом изменении шаблона.
Технически да, но это заметно больше работы, чем замена текста: нужно найти нужное место в документе, задать размеры изображения, обработать ориентацию снимков с телефона и предусмотреть отсутствующие файлы. Обычно именно на картинках самописное решение перестаёт окупаться.
Оба способа выше рабочие, код полный, ничего не вырезано. Макрос VBA выигрывает в том, что использует поиск Word — разорванные метки и формат ячеек он обрабатывает сам. Python выигрывает в независимости от Word и в скорости: сто документов за 7,4 секунды против запуска Word на каждый файл.
Бесплатное стоит времени: примерно рабочий день на старте и по паре часов при каждом изменении шаблона. Пока задача разовая или простая — это выгодная сделка. Как только появляются картинки, несколько видов документов и необходимость передать всё коллеге, счёт меняется.
Смежные материалы: как размечать шаблон Word с готовыми файлами, пределы штатного слияния, чек-лист выбора инструмента.
SmartDocGen делает то же самое без кода: берёт формат прямо из ячейки Excel, обрабатывает метки в таблицах и колонтитулах, вставляет картинки по имени файла в колонке. Условной логики и склонения падежей в нём нет — это готовится в таблице.
Бесплатного демо-режима нет: установщик скачивается свободно, но нужен код активации. Сравнить со своим скриптом дешевле всего по тарифу на месяц — 399 ₽.


