Вам в наследство досталось руководство пользователя МИС на 1160 страниц в формате .docx, тысячи повторяющихся скриншотов, процесс обновления вызывает глубокую грусть. Что с этим «добром» делать?

В этой статье я расскажу о своём проекте по переводу такой документации на рельсы docs-as-code: переосмысление структуры, укрощение размера скриншотов, борьба с кириллицей в Asciidoctor, настройка автоматической сборки HTML и PDF через GitLab CI/CD.

Сопоставление старого и нового руководства
Рис. 1. Сопоставление старого руководства и нового после рефакторинга

Наследие MS Word и синдром «талмуда»

За окном 2026 год. Представьте себе толстенный документ, набранный в MS Word 2010. Это официальное руководство пользователя для крупной медицинской информационной системы (МИС), которая ежедневно управляет потоками данных — от локальных карт пациентов до разнообразных видов взаимодействия с центральным компонентом электронной системы охраны здоровья (eHealth).

Чистый размер текстового массива составляет 1 Мб, но главная проблема в другом: документ написан кондовым языком с фиксацией на интерфейсе. Вместо того чтобы вести врача по процессам его реальной работы (оформление приёма, создание электронного направления и т.п.), «талмуд» описывает буквально каждый элемент: «При нажатии на кнопку А открывается окно Б, содержащее поле В». Когда интерфейс живой системы эволюционирует, поддержание такой книги превращается в ад: вёрстка «плывёт» от любого чиха, одновременная работа нескольких авторов невозможна, а контроль версий превращается во всем знакомое Руководство_v4_final_исправлено_copy(2).docx.

Как человек с бэкграундом в системном администрировании и опытом литературной правки, я не мог спокойно смотреть на это. Документация должна жить по законам разработки, и я инициировал пилотный проект по переработке и миграции этого руководства на рельсы "документация как код".

Выбор инструментария для работы

В мире docs-as-code есть три популярных пути, но для масштабной технической книги в данном случае подошёл только один.

  1. Markdown. Слишком примитивен. Попробуйте сверстать на нём большой многостраничный документ с перекрёстными ссылками между главами, сложной структурой модульных включений (придерживаясь принципа «единого источника») и кастомными стилями — вы быстро упрётесь в ограничения синтаксиса или зоопарк несовместимых диалектов.

  2. DITA / DocBook (XML). Профессионально, мощно, но неоправданно дорого и избыточно для гибкой разработки. XML-теги превращают чтение исходного кода в мучение, а порог входа для авторов весьма высок.

  3. AsciiDoc (Asciidoctor). Легковесный разметчик, который читается глазами так же легко, как Markdown, но обладает мощью семантических блоков, родной поддержкой включений (include::[]), условий, сложной вёрстки таблиц и готовым рендерингом в HTML и PDF «из коробки» (через плагины).

Инструменты для проекта разворачивались на обычном ноуте Lenovo ThinkPad под управлением Linux Mint. Рабочее место получилось спартанским, но максимально производительным:

  • VS Code + плагин Asciidoc в качестве основного текстового редактора.

  • Freeplane — мощная бесплатная программа для создания сложных интеллект-карт, которая ориентирована на структурирование больших объемов информации, анализ и планирование.

  • Emacs Org-mode для ведения "бортового журнала" разработчика (чтобы прочувствовать весь спектр текстового гиковства и усложнить себе жизнь).

  • Pomodorot — полезная программа, которая не даёт очманеть от работы, блокируя экран для отдыха каждые полчаса.

Планирование работы

Цель проекта формулировалась так: взять репрезентативный кусок старого руководства, безжалостно переработать его структуру под логику врача, очистить от «воды» и тяжёлых фраз, перевести в AsciiDoc и настроить автоматическую сборку через GitLab CI/CD.

Последовательность работ проекта со сроками я разработал в ProjectLibre (впоследствии в нём обнаружился огромный минус: в pdf-файл не выводится кириллица), а новую структуру документации (со ссылками на пункты исходной) — во Freeplane. При анализе структуры и её реорганизации здорово помог ИИ, без него перелопатить такой объём информации было бы куда сложнее.

Новая структура во Freeplane
Рис. 2. Новая структура во Freeplane с привязкой к пунктам старой документации

Графический квест: выковыривание и чистка 5100+ скриншотов

В подопытном документе оказалось более пяти тысяч скриншотов! Они годами копились внутри вордовского файла, сделанные в разных версиях Windows, с кучей дубликатов и диким весом pdf-ки (без малого 100 мегабайт!).

Тащить этот графический хаос в новый проект — преступление против Git-репозитория и здравого смысла. Нужен был автоматизированный процесс очистки.

Для максимальной гибкости я решил обрабатывать файлы в терминале Linux и для этой цели выбрал несколько утилит.

1. pdfimages

Первым делом я выпотрошил из документа абсолютно всю графику в PNG-формат:

$ pdfimages -png legacy-guide.pdf ./screenshots_extract

Команда работала почти 10 минут и выдала картинок аж на 588 Мб.

2. jdupes

Хранить идентичные картинки по меньшей мере глупо. Для просмотра дублей сгодилась команда, которая сравнивает файлы по контрольным суммам:

$ jdupes -rMS screenshots_extract

Её вывод был красноречив:

3295 duplicate files (in 658 sets), occupying 173 MB

Узнать число групп дублей (они разделяются пустой строкой) можно и такой командой:

$ jdupes -r /путь/к/папке | grep -c "^$"

Из любопытства захотелось узнать минимальное, максимальное и среднее количество элементов в группе. Для решения этой задачи в терминале лучше всего подошла утилита awk, которой скармливается файл с протоколом работы jdupes.

1: Получение списка размеров групп.

Команда ниже превращает файл в список чисел, где каждое число — количество строк в одной группе.

$ awk -v RS='' '{print NF}' имя_файла

RS='' — настройка awk, которая заставляет его считать разделителями записей (блоков) пустые строки;
NF — количество «полей» (слов/строк) в этом блоке.

Чтобы узнать именно количество строк, а не слов, нужно слегка изменить команду:

$ awk -v RS='' '{print gsub(/\n/, "\n") + 1}' имя_файла

В моём случае результат был одинаков.

2: Подсчёт Min, Max и Average

$ awk -v RS='' '{n = gsub(/\n/, "\n") + 1; print n}' имя_файла | \
  awk 'BEGIN {min=999999; max=0} {
         sum += $1;
         count++;
         if($1 > max) max=$1;
         if($1 < min) min=$1;    }
       END{
         if(count>0) print "Min: " min, "| Max: " max, "| Avg: " sum/count;
         else print "Групп не найдено";
       }'

Первый awk разбивает файл на блоки по пустым строкам и для каждого блока выводит одно число (количество строк внутри), а второй собирает и выводит статистику. На моих файлах получился такой результат:

Scanning: 5136 files, 1 items (in 2 specified) Min: 2 | Max: 1440 | Avg: 6,0076

Дополнительный анализ показал, что аж 68% всех групп состояли из двух файлов.

Наконец, для автоматического удаления дубликатов использовалась команда fdupes:

$ fdupes -rdN /путь/к/папке

-r — рекурсивно (искать во всех подпапках)
-d — удалять
-N — noprompt (не спрашивать подтверждение для каждого файла, оставить первый найденный).

После автоматического удаления дубликатов осталось 1840 файлов. Уже полегче!

3. findimagedupes

Для визуального сравнения и ручного удаления оставшихся дубликатов среди похожих изображений (например, один и тот же скриншот, но пересохранённый с разным сжатием или сдвинутый на пару пикселей) я использовал утилиту findimagedupes и просмотрщик pix:

$ findimagedupes  -t 98% -p `which pix` ./screenshots_extract

Эта процедура позволила убить ещё 186 файлов.

4. optipng

Оптимизировать объём файлов изображений без потери качества помогает команда:

$ find . -name "*.png" -exec optipng -o2 {} \;

Параллельная обработка на нескольких ядрах даже при значительно большем коэффициенте сжатия ускоряет процесс в 8–10 раз:

$ time find . -name "*.png" -print0 | \
       xargs -0 -P 8 -I {} optipng -o5 -strip all "{}"

Какое шаманство здесь происходит?

find . -name "*.png" -print0: ищет все PNG. Опция -print0 нужна, чтобы корректно обработать имена файлов с пробелами (в тандеме с -0 у xargs).

-P 8: запускает 8 параллельных процессов (по одному на каждый поток).

-o5 -strip all: оптимальное сжатие и полное удаление метаданных (цветовые профили монитора и прочее).

time: выводит общее время работы всей цепочки:

  • real: сколько реально времени вы просидели за чашкой чая, пока шёл процесс (именно этот показатель нам и нужен);

  • user: суммарное время работы всех ядер (оно будет в несколько раз больше real, это нормально);

  • sys: время, затраченное процессором в режиме ядра.

Можно ли ещё сильнее ужать файлы? В скриншотах программы используется много цветов из-за сглаживания шрифтов. Если после optipng объём будет всё ещё велик, можно добавить флаг -nc (no color reduction) или преобразовать изображения в индексированные цвета (если это не портит вид).

Скрипт c разделением процесса обработки на этапы и выводом результатов
#!/usr/bin/env bash

# Конфигурация
LC_ALL=C
OPTDIR=optim_imgs
TIMEFORMAT="%R"

# Подсчёт файлов
COUNT=$(find . -maxdepth 1 -name "*.png" | wc -l)
if [ "$COUNT" -eq 0 ]; then
   echo "PNG файлы не найдены в текущей папке."
   exit 1
fi

# Проверка и удаление старой папки
if [ -d "$OPTDIR" ]; then
   echo -e "\nУдалена старая папка $OPTDIR/"
   rm -rf "$OPTDIR"
fi
mkdir -p "$OPTDIR"

# Основной процесс с замером времени
echo "Оптимизация $COUNT файлов на 8 потоках..."
ELAPSED=$( { time find . -maxdepth 1 -name "*.png" -print0 | \
      xargs -0 -P 8 -I {} optipng -o5 -quiet -strip all "{}" \
      -out "$OPTDIR/{}"; } 2>&1 )

# Расчёт времени
MIN=$(awk "BEGIN {print int($ELAPSED / 60)}")
SEC=$(awk "BEGIN {printf \"%.2f\", $ELAPSED % 60}")
AVG=$(awk "BEGIN {printf \"%.2f\", $ELAPSED / $COUNT}")

# Расчёт размеров
OLD=$(du -Ssb . | cut -f1)
NEW=$(du -sb "$OPTDIR/" | cut -f1)
DIFF=$((OLD - NEW))
PERCENT=$(awk "BEGIN {printf \"%.2f\", ($DIFF/$OLD)*100}")

# Вывод протокола работы
echo -e "\n--- ПРОТОКОЛ ОПТИМИЗАЦИИ ---"
echo "Обработано файлов: $COUNT шт."
echo "Общее время:       $MIN мин. $SEC сек."
echo "Среднее на файл:   $AVG сек."
echo "----------------------------"
echo "Исходный объём: $(numfmt --to=iec $OLD)"
echo "Новый объём:    $(numfmt --to=iec $NEW)"
echo "Экономия:       $(numfmt --to=iec $DIFF) ($PERCENT%)"

Файлы после обработки складываются в папку optim_imgs в текущей папке. При перезапуске скрипта она удаляется и пересоздаётся автоматом. После отработки скрипта получился такой вывод:

Удалена старая папка optim_imgs/
Оптимизация 1654 файлов на 8 потоках…
— ПРОТОКОЛ ОПТИМИЗАЦИИ —
Обработано файлов: 1654 шт.
Общее время: 27 мин. 35,69 сек.
Среднее на файл: 1,00 сек.
----------------------------
Исходный объём: 408M
Новый объём: 388M
Экономия: 21M (5,00%)

В итоге скриншотов стало втрое меньше (1654 вместо 5136), а их объём уменьшился на 200 МБ. Основную экономию дала очистка дубликатов, а optipng — ещё 5%. Файлы получили сквозную нумерацию, готовую к импорту в AsciiDoc.

Грабли с кириллицей

Когда структура проекта устаканилась и был готов первый adoc-файл, пришло время компиляции в PDF. И тут я столкнулся с суровой реальностью open-source инструментов, изначально заточенных под английский язык.

Попытка собрать документ дефолтным вызовом asciidoctor-pdf mis-doc.adoc выдало гирлянду сообщений: WARNING: The following text could not be fully converted to the Windows-1252 character set, а в pdf-файле — мешанину крокозябр вместо букв. Родные шрифты движка не имели понятия о существовании кириллицы.

Решением было создать кастомную тему оформления в формате YAML и подключить TTF-шрифты с кириллицей (я взял проверенные семейства Liberation). Для лучшей переносимости проекта я положил шрифты в отдельную локальную папку.

Фрагмент конфигурационного файла theme.yml со шрифтами:

font:
  catalog:
    merge: false
    LiberationSans:
      normal: LiberationSans-Regular.ttf
      bold: LiberationSans-Bold.ttf
      italic: LiberationSans-Italic.ttf
      bold_italic: LiberationSans-BoldItalic.ttf
    LiberationMono:
      normal: LiberationMono-Regular.ttf
      bold: LiberationMono-Bold.ttf
      italic: LiberationMono-Italic.ttf
      bold_italic: LiberationMono-BoldItalic.ttf
    M+ 1mn:
      normal: mplus1mn-regular-subset.ttf
      bold: mplus1mn-bold-subset.ttf
      italic: mplus1mn-italic-subset.ttf
      bold_italic: mplus1mn-bold_italic-subset.ttf
    fallbacks:
    - M+ 1mn
    main:
      family: LiberationSans

Asciidoctor-pdf при наследовании темы (extends: default) рассчитывает найти стандартные шрифты для кода и кнопок. Если вы отключаете слияние каталогов (merge: false), но не описываете эти шрифты в своём файле, движок впадает в панику. Поэтому пришлось положить в папочку и шрифт M+ 1mn.

Очень рекомендую проверять реальные имена шрифтов с помощью утилиты fc-query. Найти установленные шрифты с кириллицей можно так:

fc‑list :lang=ru family file

Ну а если что-то не работает — читайте предупреждения в консоли (asciidoctor-pdf выводит их в stderr), а для вывода деталей об ошибках добавьте ключ --trace.

Итак, сборка завелась, но тут же вылезла вторая проблема: конфликт абзацных отступов. Чтобы книга выглядела солидно, я включил в тему отступ для первой строки абзаца:

prose:
  first-line-text-indent: 18   # НЕДОКУМЕНТИРОВАННЫЙ ПАРАМЕТР!
  margin-bottom: 8      # уменьшаем стандартный отступ после абзаца

Однако теперь этот отступ применяется вообще ко всему, включая текст внутри ячеек таблиц и элементы списков. Таблицы стали выглядеть странно. Пришлось выкручиваться на уровне разметки: либо использовать тип форматирования ячеек d| (literal/raw data), который полностью сбрасывает стилизацию абзаца, либо оформлять вводные фразы перед списками как заголовки списков, защищая их от отступов. Что ж, красота требует жертв.

Борьба с галлюцинациями ИИ

Один из поучительных уроков этого проекта — не верить нейросетям на слово, когда дело касается специфических YAML-конфигураций. В большинстве случаев он чертовски помогал, но было и так, что бесцеремонно врал: например, предлагал ключи с подчеркиваниями вместо дефисов (font_family вместо font-family, page_size вместо page-size и т.д.). На сайте документации Asciidoctor я нашёл лишь одно исключение из общей схемы: bold_italic.

Мораль здесь такова: ИИ — отличный подмастерье для генерации идей и «рыбы» вашей документации, но синтаксис специфических предметных DSL-языков за ним нужно перепроверять буквально с лупой.

Пайплайн сборки в GitLab CI/CD

Облачным репозиторием был выбран GitLab, поскольку там приватные проекты можно размещать бесплатно. Я настроил лёгкий пайплайн .gitlab-ci.yml, использующий официальный Docker-образ asciidoctor/docker-asciidoctor:latest. При каждом git push система автоматически генерирует свежий PDF-документ и публикует статический HTML-сайт в GitLab Pages:

image: asciidoctor/docker-asciidoctor:latest
stages:
  - build
  - deploy

# Сборка  PDF
build_pdf:
  stage: build
  script:
    - asciidoctor-pdf -r asciidoctor-diagram mis-doc.adoc -o mis-user-guide.pdf
  artifacts:
    name: "docs-pdf-${CI_COMMIT_REF_SLUG}"
    paths:
      - mis-user-guide.pdf
    expire_in: 1 week
  only:
    - main  # сборка выполняется при обновлении ветки main

# Публикация на GitLab Pages
pages:
  stage: deploy
  script:
    - mkdir public
    - asciidoctor -r asciidoctor-diagram mis-doc.adoc -o public/index.html
    # скопировать PDF в ту же папку, чтобы его можно было скачать по ссылке
    - cp mis-user-guide.pdf public/ || true
    # скопировать папку со скриншотами в public, чтобы сайт их увидел
    - cp -r images public/ || true
  artifacts:
    paths:
      - public
  only:
    - main

Итоги проекта

Этот проект показал, что MS Word плохо подходит для больших руководств в плане гибкости и сопровождения. Рефакторинг текста и перевод документации на технологию docs-as-code коренным образом изменил ситуацию:

  • Вместо описания кнопок интерфейса — описание логики сценариев работы врача.

  • Текст после рефакторинга сократился в 3,5 раза за счёт смысловой оптимизации.

  • Скриншотов стало втрое меньше, а их общий объём сократился более чем на треть.

  • Вся документация теперь хранится в обычном Git-репозитории, легко версионируется, поддаётся сквозному поиску через grep и готова к быстрому внесению изменений под обновляющиеся требования eHealth.

  • Интерактивное руководство пользователя (HTML) и его печатная PDF-версия собираются в CI/CD за несколько минут.

Asciidoctor в связке с простыми консольными утилитами Linux и GitLab CI/CD показал себя как мощный инструмент. Использование диаграмм типа PlantUML в ряде случаев позволит заменить текстовое описание процедур наглядными схемами.

Одностраничный HTML с оглавлением выглядит очень прилично, а с генератором сайтов Antora пользоваться руководством станет ещё удобнее.

Комментарии (0)