- BrainTools - https://www.braintools.ru -

Хабр, привет! Меня зовут Ману, и я бывший фронтенд-разработчик, который уже семь лет работает корпоративным юристом. Да, звучит странно. Но именно эта комбинация помогает в моей повседневной работе, с которой сталкивается каждый практикующий юрист: бесконечное заполнение одних и тех же форм, где отличаются только наименования юрлиц и их реквизиты.
В этой статье я расскажу, как из Excel-таблиц пришел к Python-скрипту, который генерирует десятки документов за пару секунд. Эта история о том, как технический бэкграунд помогает использовать ИИ не просто для генерации текстов, а создавать инструменты, которые реально экономят часы рутинной работы. Прежде всего для минимизации фактора человеческой ошибки [1] и автоматизации рутины.
Начну с предыстории. Под конец 9-го класса я увлекся Minecraft, но не самой игрой, а возможностью создать свой проект под нее. Это были 2012-2015 годы, когда индустрия игровых серверов переживала настоящий бум. Сидя на форумах и изучая, как собрать лаунчер и сайт для своего проекта, я решил начать с дизайна. Так меня и втянуло в веб-разработку.
Около шести лет я проработал в небольшой студии верстальщиком и фронтендером, хотя второе можно было назвать с большой натяжкой. Потом началась взрослая жизнь, и надо было думать о чем-то более серьезном. Меня пригласили в юридическую фирму стажером просто потому, что я разбирался в компьютерах и знал, что такое Word. Да, тогда с наймом было чуточку проще :)
С 2019 года началась моя полноценная юридическая практика. Я полностью ушел из разработки и переключился на юриспруденцию. Работаю в основном с корпоративным и миграционным правом. И вот тут началось самое интересное.
Я быстро понял, что работа юриста делится на два типа. Первая – это более творческий подход, где включается и работает мозг [2] на полную. Сюда относится все, что связано с аналитикой: составление стратегии защиты в суде, написание юридических заключений с анализом нестандартных ситуаций, разработка сложных договорных конструкций. А вторая – формализированная бюрократическая рутина, когда ты просто заполняешь типовые договора, заявления, ходатайства для государственных органов. И вот вторая часть съедала львиную долю времени.
Еще в начале карьеры я устал копировать одни и те же данные из документа в документ. Наименование юридического лица тут, его адрес там, ОГРН в третьем месте – и так по кругу. Тогда мне пришла идея сверстать типовые бланки в Excel и объединить их в один файл по разным вкладкам.
Схема была простая:
На первой вкладке (мастер-вкладка) я заполнял всю информацию о клиенте;
Со второй и по предпоследнюю (верстка) шли готовые формы документов;
А последняя вкладка (техническая) работала как мозг всей системы.

Техническая вкладка забирала данные с первой, унифицировала их, форматировала и так далее и выдавала конечный результат. Также тут были прописаны условия вроде «если поле пустое, выведи прочерк».
После заполнения я просто переходил на нужную вкладку и печатал документ напрямую из Excel. Конечно, были сложности с версткой, потому что печать из Excel – не тоже самое, что и из Word. Приходилось возиться с отступами и размерами ячеек, чтобы все красиво укладывалось на стандартный лист А4.
Однако, этот подход можно было использовать до поры до времени. Но проблема в том, что формы живут своей жизнью. Ведомства обновляют приказы, вводят новые бланки, меняют старые. И каждый раз нужно было переверстывать Excel-файл. Учитывая сложность верстки, руки доходили до этого не сразу – проще было вручную забить данные в новый документ. Со временем форм становилось все больше, и система начала трещать по швам. Нужно было придумать что-то кардинально новое.

Возвращаться обратно к Excel не хотелось. Требовалось унифицированное решение, которое можно было бы масштабировать и передать коллегам без особых проблем. Плюс использовать не только в моей практике, но и других областях. В голове сразу возникла идея: берем регламентированные формы в PDF, сохраняем их и накладываем текст поверх через SVG. Реализовать это можно было бы через обычное SPA-приложение на React.
Но когда я начал прикидывать объем работы, энтузиазм быстро остыл. Нужно было:
Упорно и долго писать код на JavaScript;
Разбираться в тонкостях React, и других библиотеках для работы с SVG или canvas;
Сделать превью документа;
Придумать интерфейс для разметки форм;
И много еще чего, включая дебаг и небыстрое начало к самой сути – заполнения документов.
Проблема усложнялась тем, что в некоторых государственных формах текст пишется строго побуквенно по клеткам. Представьте себе: поле для названия организации – это 34 отдельные ячейки, в каждую нужно вписать по одной букве. Программно это означало необходимость распознавать эти клетки и корректно заполнять каждую. Для меня это не было невыполнимой задачей, но я хотел масштабировать решение на всю команду, чтобы другие юристы могли пользоваться без необходимости разбираться в технических деталях.
Эта идея очень долго висела в моей голове мертвым грузом. Я на протяжении нескольких лет раздумывал как подступиться к этой проблеме и с чего начать. Были разные мысли о том, как группировать данные по категориям (например, база юридических лиц, база физических лиц) и при создании нового проекта просто выбирать стороны, параметры и генерировать нужные документы. Но до реализации так и не доходило.
Время шло, формы менялись и добавлялись новые. И тут в наше бюро пришел клиент – запрос объемный и с большой типовой документацией. Откладывать больше уже было нельзя.
Все новое – хорошо забытое старое. Думал я, и решил пойти проверенным путем: скормить вордовский документ ИИ и посмотреть, что получится. Результат меня не удивил – он выдал непонятное что вместо верстки, и его нельзя в этом винить. Он не видит то, что видит человек. В его мире алгоритмов – все сделано в соответствии с правилами.
Тогда в наших юридических документах, где требовался аналитический подход, я вовсю использовал Элементы управления контентом (ContentControl). Но его применение было по минимуму: выбрать дату из календарика, вместо ручного прописывания; выбор формы юридического лица (АО или ООО); список с арбитражными судами и их адресами; выбор поверенных лиц и так далее. Простые вещи, упрощающие работу, но не более того.
И тут меня осенило: а что, если использовать ContentControl на полную катушку? Идея была такая:
в папке проекта у нас лежит мастер-файл (вордовского формата), внутри него уже приличная и красивая таблица, где слева указаны название полей (наименование, ОГРН, ИНН и т.д.), а справа само поле ContentControl для ввода данных;
рядом с мастер-файлом находятся подпапки с формами для разных инстанций;
сами формы внутри также размечены полями ContentControl, но их идентификаторы полностью совпадают с идентификаторами из мастер-файла.
project/
│
├── master.docx # Мастер-файл с данными
├── script.py # Скрипт обработки
│
├── forms_tax/
│ ├── *.docx # Формы с ContentControl
│ ├── *_map.json # Конфиги (если нужны)
│ └── PDF/ # ← создается скриптом
│
├── forms_migration/
│ ├── *.docx
│ └── PDF/
│
└── forms_court/
├── *.docx
└── PDF/
А дальше в дело вступает скрипт на Python, который и является мозгом всей этой затеи. Запускаем через консоль скрипт, и он проходится по всем подпапкам, смотрит вордовские файлы и вставляет данные из мастер-файла в соответствующие поля с одинаковыми идентификаторами. Если в процессе выполнения не обнаружены ошибки, то в каждой подпапке создается папка PDF, где в них будут располагаться сгенерированные PDF-документы.
Сначала я хотел положить все формы в один вордовский файл и обновлять поля через VBA, но быстро от этой затеи отказался из-за разных типов полей в формах (об этом – далее).
Реализовать задуманное через Python, как я видел, можно посредством вызова функции Word или LibreOffice. Однако, проблемы возникли и тут. При запуске, Word сильно капризничал и каждый раз хотел права на просмотр папок и редактирование файлов, тогда как LibreOffice – нет. Пытался также сделать вызов Word в «тихом» режиме без запуска GUI, но мой скрипт сломался. И на данный момент я решил полностью остановиться на LibreOffice. Буду благодарен, если знающие специалисты подскажут как в будущих итерациях отказаться от этой зависимости и использовать только open-source библиотеки для Python.
При первых тестах и пробах уже вырисовывался контур этой архитектуры. Она была понятной и простой, не требовала значительного времени и усердной верстки. Буквально за 4-6 часов я размечал и тестировал 12 шаблонов, тогда как на верстку одного документа у меня уходило по несколько суток.
Итак, технически, формат docx – это обычный zip-архив с XML внутри. Если вы когда-нибудь смотрели на древо веб-страницы в DevTools, то структура вордовского документа покажется знакомой. Те же вложенные теги, те же атрибуты, только namespace другой. Библиотека python-docx предназначенная для создания, чтения, редактирования вордовских файлов и позволяет работать с ними напрямую, не открывая Word вообще.
Скрипт при запуске читает мастер-файл, собирает все ContentControl-поля и их значения в словарь (по аналогии с объектами в JavaScript), где ключ – это идентификатор поля, а значение – то, что вы туда вписали. Дальше он обходит все подпапки, находит размеченные вордовские файлы и в каждом из них заменяет поля с совпадающими идентификаторами на нужный текст. После этого запускается LibreOffice (без GUI, просто прыгающая иконка) и генерируются PDF. Пока не дошел до способа, чтобы приложение вообще не запускалось, а работало только через консольные команды.
Не сказать, что все обошлось без ошибок и проблем. Word хранит текст внутри XML весьма своеобразно. Простая фраза [company_name] может быть разбита на несколько отдельных XML-элементов, каждый со своим форматированием. В структуре XML-файла вордовского документа текст внутри абзаца (<w:p>) разбивается на так называемые «прогоны» (runs) – элементы <w:r>.
Это примерно, как если бы текст в теге <p> был разбит на десяток <span> без видимой причины. Поэтому простая замена строки не работала. Чтобы корректно обновить данные, было решено собирать текст всего абзаца воедино, выполнять замену и перезаписывать содержимое, предварительно очищая структуру от лишних «прогонов».
Когда разобрался с обычным текстом, пришел черед нестандартных ситуаций. Например, некоторые государственные формы содержат таблицы, где каждая буква пишется в отдельную клетку. Поле «наименование организации» это 34 клетки подряд, каждая отдельная ячейка в XML. Размечать такое через ContentControl – это сизифов труд. Нужно было искать другое решение.
Для таких форм я ввёл дополнительный файл конфигурации имя_файла_map.json. Простой JSON, где для каждого поля указывается в какие таблицы и ячейки писать символы. Скрипт читает значение из мастер-файла и последовательно вписывает каждый символ в соответствующую ячейку. Чтобы не высчитывать индексы ячеек вручную, была разработана утилита, которая генерирует размеченную версию документа. Суть заключалась в том, что в каждой пустой ячейке красным мелким шрифтом пишется её индекс. Открываешь размеченный документ в Word, смотришь нужные координаты и прописываешь их в map-файл.
В итоге получилось три типа полей, с которым скпт умеет работать:
ContentControl для стандартных форм и простой разметки;
Побуквенное заполнение для нестандартных форм;
Гибридный вариант, сочетающий оба подхода.
Отдельно пришлось повозиться с датами. В государственных формах дата рождения – восемь клеток (две под число, две под месяц, четыре под год). Для некоторых ячеек под год выделено не четыре клетки, а две. Поэтому в map-файле для дат есть отдельная секция, где явно указывается какие ячейки под что. Скрипт берёт дату из мастер-файла в формате ДД.ММ.ГГГГ, разбивает её и раскладывает по нужным индексам.
Была и ещё одна нетривиальная проблема. Это когда одно ContentControl-поле находится внутри другого, такое случается если копировать и вставлять поля в Word. Визуально всё выглядит нормально, но при открытии сгенерированного файла Word выдавал предупреждение о повреждённом содержимом. XML был синтаксически валидным, но семантически нет, потому что вложенные поля – это неподдерживаемая конструкция. Пришлось добавить предварительную обработку, чтобы перед заполнением скрипт разворачивал все вложенные поля, заменяя их на простой текст.
Про форматирование стоит сказать отдельно. При заполнении некоторых форм я придерживаюсь единообразия: заглавные буквы, жирный шрифт и конкретный размер. Если в мастер-файле мы пишем как обычно (первая буква заглавная, а остальные строчными), то он так и переносится в другие формы. Захардкодить это в скрипте было бы неправильно, потому что в разных документах требования разные. Поэтому в map-файле появилась секция _format где для каждого поля можно указать свои параметры (upper переводит текст в верхний регистр, bold делает жирным, size задаёт размер шрифта, align выравнивание). Если форматирование явно не указано в map-файле, то применяются дефолтные значения. Работает одинаково и для побуквенных ячеек и для ContentControl-полей.
{
"_comment": "Маппинг для Ходатайства РР ВКС",
"_format": {
"inn": { "size": 8 },
"okato": { "size": 8 },
"passissue": { "upper": true, "bold": true, "size": 9, "align": "center" },
"polis_issued": { "upper": true, "size": 9 }," }
},
"_dates": {
"ogrn_date": {
"day": [[13, 15], [13, 16]],
"month": [[13, 17], [13, 18]],
"year": [[0, 0], [0, 0], [13, 19], [13, 20]]
}
},
"inn": [[11, 1]],
"ogrn": [[13, 1]],
"okato": [[11, 12, true]],
"company_phone": [[20, 1]]
}
Ещё одна проблема, которую я не сразу предвидел: количество ячеек в форме не всегда совпадает с длиной текста. Поле ОКАТО это одиннадцать символов, а ячеек под него десять – скорее всего при разработке формы просто не посчитали. Первое время это решалось вручную и было неудобно. В итоге в формат записи map-файла добавился третий параметр (булев). Если стоит true и символов больше, чем ячеек, скрипт идёт с конца строки и добавляет последний символ в крайнюю ячейку. Выглядит это так: [11, 1, true] – таблица 11, начиная с ячейки 1, с переносом остатка в последнюю клетку. Скрипт берёт строку, нарезает её кусками нужной длины и раскладывает по ячейкам.
Причём это не мое решение проблемы – это требование инспектора уполномоченного органа. Некоторые коллеги пошли другим путём и просто добавили лишнюю ячейку в таблицу, чтобы влезли все символы. Казалось бы логично [4], но их документы признали недействительными – форма не совпадала с утверждённым регламентом. Бюрократия есть бюрократия.
Реализацию скрипта я полностью отдал искусственному интеллекту [5] (решил проверить на деле Claude Code). В процессе дебага и разработки нужно было лишь корректировать его и направлять в нужно русло. Конечно, задумка не доведена до идеала, но с ним моя команда уже сэкономила уйму времени и снизила вероятность человеческой ошибки при заполнении документов.
Хочется более унифицированного и масштабируемого решения, а не дублирования шаблонов и мастер-файла в каждый новый проект. В будущих планах отказаться от мастер-файла. Например, перейти на работу с библиотекой python-docx-template, которая умеет подставлять данные прямо в шаблоны на основе переменных. Или, что ещё лучше, сделать простое SPA-приложение, где можно создавать проекты, вбивать реквизиты сторон, выбирать нужные документы из каталога и одним кликом генерировать их. По сути, довести текущую идею до полноценного конструктора документов.
Но когда я начал об этом думать, быстро понял, что автоматизация заполнения форм – это только вершина айсберга. Да, мы научились делать массово нужные нам документы, но что происходит с ними дальше? Они либо уходят в архив на локальном диске, либо распечатываются и подшиваются в папки. А если через месяц нужно найти конкретный договор или проверить, не истёк ли срок действия доверенности? Без нормальной системы учёта начинается тот самый ад с Excel-табличками, который мы все так любим.
Поэтому следующим логичным шагом вижу не просто улучшение генератора, а создание полноценной системы учёта договоров и документооборота. Представьте: цифровой реестр договоров с удобными тегами и категориями, чтобы за пару кликов находить всё, что связано с конкретным клиентом или судебным процессом. Хранилище скан-копий с шифрованием, контроль версий документов (как в Git), автоматические напоминания о дедлайнах, шаблонизация и даже OCR с NLP-анализом текста, адаптированным под российское законодательство.
Пока что мои коллеги ведут документооборот по старинке: нумерация в Excel, а сканы просто лежат в папках с названиями вроде «договоры_2025». Если нужно что-то найти – начинается перебор. Хотелось бы, чтобы отечественная юриспруденция не отставала от технологий и предлагала удобные инструменты как для небольших команд, так и для крупных фирм. Короче, планов много. Начну, наверное, с прототипа на python-docx-template, а дальше видно будет.
Оглядываясь назад, понимаю, что каждый мой опыт [6] в итоге сыграл свою роль. Получилось не запланированно, но в какой-то момент знания из совершенно разных сфер начали работать вместе. И сейчас мне уже кажется странным, что так мало людей используют этот синтез.
Когда начинаешь автоматизировать хотя бы часть этого процесса, выясняется две вещи. Первая: времени освобождается реально много. Вторая: это затягивает. Сначала делаешь простой скрипт, а потом ловишь себя на мысли, что хочешь переписать всю систему учёта договоров под себя.
И вот тут я упираюсь в главную проблему. На данный момент для небольших юридических команд и юристов-предпринимателей практически нет готовых инструментов, которые закрывали бы повседневные потребности [7]. Либо это монструозный 1С:Документооборот, рассчитанный на большой отдел, либо самописные Excel-таблицы, которые рано или поздно начинают сыпаться. Золотой середины, к сожалению, почти нет.
Если у вас есть опыт автоматизации в юридической или другой профессиональной сфере, буду рад обсудить в комментариях.
---
Напиши Python скрипт `fill_documents.py` для автоматического заполнения Word-документов данными из мастер-файла с последующей конвертацией в PDF через LibreOffice headless.
**Архитектура:**
Есть папка проекта где лежит `master.docx` — Word-документ с ContentControl-полями (тег каждого поля это его идентификатор, например `company_name`, `inn`, `surname`). Рядом лежат подпапки с шаблонами документов. Скрипт читает данные из мастера и заполняет все найденные шаблоны, после чего конвертирует каждый в PDF и кладёт в подпапку `PDF/`.
**Типы полей которые должны поддерживаться:**
Первый тип — ContentControl (sdt). Скрипт находит их по тегу и заменяет значение. Перед заполнением необходимо разворачивать вложенные sdt — Word иногда создаёт sdt внутри sdt при копировании полей, и это делает итоговый файл невалидным.
Второй тип — текстовые плейсхолдеры вида `[field_name]` прямо в тексте параграфов. Проблема в том что Word внутри XML может разбить строку `[company_name]` на несколько отдельных `<w:r>` элементов с разным форматированием. Поэтому нужно склеивать весь текст параграфа из всех ранов, делать замену и записывать результат в первый `<w:t>`, зануляя остальные.
Третий тип — побуквенные таблицы. В некоторых госформах каждый символ пишется в отдельную ячейку таблицы. Для таких документов рядом с шаблоном кладётся файл `имя_документа_map.json` — если он найден, скрипт переключается в режим побуквенного заполнения.
**Формат map.json:**
Секция `_format` — форматирование для каждого поля: `upper` (bool), `bold` (bool), `size` (int, пункты), `align` (center/left/right). Если поле не указано в `_format` — применяются дефолтные значения `bold: true, size: 10, align: center, upper: false`. Форматирование применяется ко всем типам полей включая ContentControl.
Секция `_computed` — вычисляемые поля по шаблону: `"company_full_name": "{org_form} "{company_name}""`. Скрипт вычисляет их из других полей данных перед заполнением.
Секция `_dates` — для полей где дата разбита по отдельным ячейкам. Формат: отдельно перечисляются ячейки под день, месяц и год. Каждый элемент это конкретная ячейка `[таблица, ячейка]` — один элемент равно один символ. Дата в мастере хранится в формате `ДД.ММ.ГГГГ`.
Секция `_text` — текст целиком в одну конкретную ячейку, без побуквенного разбиения.
Обычные поля — побуквенное заполнение. Формат записи: целое число `49` означает всю таблицу с нулевой ячейки. Массив `[49, 1]` означает таблицу 49 начиная с ячейки 1. Массив `[49, 1, true]` означает таблицу 49 начиная с ячейки 1, и если символов больше чем ячеек — последний символ добавляется в крайнюю ячейку.
**Конвертация в PDF:**
Через LibreOffice headless: `soffice --headless --convert-to pdf`. Скрипт ищет LibreOffice по стандартным путям для macOS, Linux и Windows. LibreOffice создаёт PDF в директории источника — скрипт затем перемещает его в папку `PDF/`.
**Вспомогательные утилиты:**
`mark_tables.py` — принимает путь к docx, создаёт копию где в каждой пустой ячейке таблицы красным шрифтом размером 5pt написан индекс в формате `таблица:ячейка`. Используется для составления map.json вручную.
**Зависимости:** только `python-docx` и `lxml`. LibreOffice должен быть установлен отдельно.
**Обработка ошибок:** файл map.json читать с кодировкой `utf-8-sig` чтобы корректно обрабатывать BOM который добавляют Windows-редакторы. При ошибке конкретного файла скрипт логирует её и продолжает обработку остальных.
---
Автор: mnkh
Источник [8]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/26754
URLs in this post:
[1] ошибки: http://www.braintools.ru/article/4192
[2] мозг: http://www.braintools.ru/parts-of-the-brain
[3] Image: https://sourcecraft.dev/
[4] логично: http://www.braintools.ru/article/7640
[5] интеллекту: http://www.braintools.ru/article/7605
[6] опыт: http://www.braintools.ru/article/6952
[7] потребности: http://www.braintools.ru/article/9534
[8] Источник: https://habr.com/ru/articles/1007750/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1007750
Нажмите здесь для печати.