Иногда возникает задача создания множества текстовых документов по общему шаблону, с подстановкой значений полей из таблицы. Это могут быть приказы, заявления, акты. Я тоже столкнулся с такой задачей — нужно было создать 6 тысяч файлов .doc из одного образца паспорта прибора, подставив в каждый лишь номер прибора.

Сначала взялся делать вручную — но быстро понял что это потребует жутких усилий и чревато ошибками. Искал всевозможные генераторы документов, но все они или платные, или какие-то не такие. Пробовал обращаться из программы на Delphi к окну Word через OLE, но это тоже было довольно криво и медленно.

В итоге я нашёл гениальное решение, которое позволило мне легко генерировать по 2 тысячи документов в час на Atom-ном сервере.

Ключ к решению — формат .docx.

Как мы помним, раньше Microsoft использовала бинарный формат хранения данных, который по мере развития всё больше напоминал процессоры x86 — такой же медленный, раздутый, запутанный, кишащий устаревшими и странными вещами, какими-то обрывками реализации в своё время перспективных фич, но не нужных сейчас. И самое главное — это делало невозможным создание сторонних парсеров/генераторов документов.

Все существовавшие сторонние читалки реализовывали лишь часть функционала, пропуская порой по 90% текста, либо отображая его в неверной кодировке. Самое смешное, что в итоге Microsoft неоднократно ломала совместимость между версиями. Продолжаться так не могло, и в 2007 году появился новый формат docx, представляющий собой просто zip-архив нескольких xml-файлов — параметры документа, текст, стили, включённые объекты и макросы.

Разбор .docx-файла

Для начала, нужно перевести шаблон в формат docx, т.к. мой образец был в формате doc. Потом — изменить расширение на zip, и распаковать любым архиватором, например в папку doc.

Не буду разбирать строение всего файла, ограничусь сутью. Открываем файл word/document.xml, видим большую xml-структуру. Находим поле для замены (в моём случае это было «__________»), убеждаемся что оно есть и доступно для простого полнотекстового поиска.

Программа-генератор docx

Ну а теперь пишем node.js-скрипт, который сначала прочитает в память все файлы этого архива, потом создаст в памяти zip-объект из этих файлов, а потом в цикле будет заменять искомое поле на очередное значение из файла task.json и сохранять результат.

var fs = require('fs');
var zip = new require('node-zip')();

/* читаем файлы архива в память */
f01 = fs.readFileSync('doc/_rels/.rels');
f02 = fs.readFileSync('doc/docProps/app.xml');
f03 = fs.readFileSync('doc/docProps/core.xml');
f04 = fs.readFileSync('doc/word/_rels/document.xml.rels');
f05 = fs.readFileSync('doc/word/theme/theme1.xml');
f06 = fs.readFileSync('doc/word/document.xml');
/* тут все остальные файлы */

/* создаём zip-объект */
zip.file('_rels/.rels', f01);
zip.file('docProps/app.xml', f02);
zip.file('docProps/core.xml', f03);
zip.file('word/_rels/document.xml.rels', f04);
zip.file('word/theme/theme1.xml', f05);
zip.file('word/document.xml', f06);
/* тут все остальные файлы */

/* читаем задание в виде файла json: [ {"addr": 1, "numb": 4897656}, {"addr": 2, "numb": 5735967} ] */
task=JSON.parse(fs.readFileSync('task.json', {encoding: 'utf8'}));
console.log('task was readed');

/* замена поля на значение, и сохранение файла под нужным именем */
var write_record = function(filename, counter, all)
{
  zip.file('word/document.xml', String(f06).replace("______________________", "______"+counter+"_______"), {mode: 0777});
  fs.writeFileSync("result/"+filename+"_act.docx", zip.generate({base64: false, compression: 'DEFLATE'}), 'binary');
}

/* проходим циклом по заданию */
for(i=0; i<task.length; i++)
  write_record(task[i].addr, task[i].numb, task.length);

По сути, мы не генерируем docx вручную (это и не нужно), а просто разбираем архив с исходным docx, заменяем нужную строчку и собираем архив обратно.

На процессоре Atom 330 одна запись (шаблон размером 200кБ) обрабатывается за 2 секунды.

А самое, самое классное — в итоге получается массив файлов с крайне высокой степенью похожести, который сжимается в 300 раз и более (с помощью 7zip).