Заполнение документа Word без ComОбъект

Программирование - Практика программирования

74
Идея написать публикацию пришла после прочтения очередного рассказа о том, как файл Word заполнялся через COM-объект в клиент-серверном варианте. При этом падал Сервер 1С. Зачем в принципе использовать файлы Word как шаблоны? Ну, допустим, в организации используется некая внутренняя отчетность, выполнения в корпоративном стиле, и переделать ее на привычные табличные документы нет возможности.

Приветствую. Предлагаю вариант решения заполнения файла Word, без участия стороннего ПО, только нативные средства платформы 1С.

Известно, что файл *.docx упакованный контейнер с набором xml и медиа файлов. Но об *.doc такого сказать нельзя, это скорее бинарник. Файл docx легко распаковывается архиватором, а doc нет. А раз распаковывается и внутри у него знакомые xml, то почему бы не попробовать изменить xml программно? Необходимые средства для выполнения этой задачи платформа имеет.

Сначала попробуем разобраться как же устроен docx изнутри. Создадим новый файл Word с текстом - "Просто текст, а это МойПараметр". Затем распакуем файл и увидим следующую иерархию данных.

 

О назначении каждого из файлов хорошо написано тут. Из всех файлов для решения нашей задачи интересен только один. Адрес файла - word/document.xml. Открываем файл и видим, что содержимое не отформатировано и для визуального анализа не подойдет. Отформатировать xml онлайн можно тут или же использовать Notepad++. Дальше по ключевому слову МойПараметр находим параметр и видим примерно следующее.

<w:r>
    <w:rPr>
        <w:lang w:val="ru-RU"/>
    </w:rPr>
    <w:t>Просто текст</w:t>
</w:r>
<w:r w:rsidR="004E0307">
    <w:rPr>
        <w:lang w:val="ru-RU"/>
    </w:rPr>
    <w:t>, а это МойПараметр</w:t>
</w:r>

После небольшого анализа понятно, что текст находится в тегах t с префиксом w (стоит сказать, что все теги файла имеют этот префикс. Это пространство имен http://schemas.openxmlformats.org/wordprocessingml/2006/main). Для упрощения задачи, будем считать, что параметры шаблона всегда будем писать "верблюжьей" нотацией, потому как, если например написать вот так [Параметр1], то в xml это будет три тега <w:t>, с разными родительскими тегами <w:r>, что существенно усложнит алгоритм замены. Таким образом, чтобы программно заменить параметр на нужный текст, нужно найти тег <w:t> с текстом параметра и выполнить замену. Потом файл с замененными параметрами сохранить и упаковать в архив.

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

Создадим обработку, у которой в макет с типом ДвоичныеДанные положим наш подопытный файл docx. Дальше создадим форму, команду формы и для команды элемент управления. По нажатию на кнопку передадим управление на сервер и начнем писать код. Для начала получим шаблон из макета обработки.

BinaryData = FormAttributeToValue("Object").GetTemplate("Template");

Так как тип макета ДвоичныеДанные, то и получим также двоичные данные. Дальше нужно полученные данные распаковать. Для этого воспользуемся объектом ЧтениеZipФайла. Объект инициализируется на основании объекта ПотокВПамяти, а поток в памяти создается на основании объекта БуферДвоичныхДанных, который получается из метода ПрочитатьВБуферДвоичныхДанных объекта ЧтениеДанных, который создается на основании двоичных данных, которые у нас есть.

Reader 			 = New DataReader(BinaryData);	
BinaryDataBuffer = Reader.ReadIntoBinaryDataBuffer();

Stream = New MemoryStream(BinaryDataBuffer);
	
ZipReader = New ZipFileReader(Stream);
ZipReader.ExtractAll(Dir);
ZipReader.Close();
	
Stream.Close();
Reader.Close();

Теперь прочитаем document.xml. Прочитать можно разными способами, но мы воспользуемся ПостроительDOM, который вернет объект ДокументDOM.

XMLReader = New XMLReader;
XMLReader.OpenFile(PathXMLFile);
	
Builder 	= New DOMBuilder;
DomDocument = Builder.Read(XMLReader);
XMLReader.Close();

Теперь самое интересное - поиск тегов <w:t>. Можно это сделать разными способами, вплоть до обхода ДокументDOM как дерева. Но существует способ проще. Поскольку тегов <w:t> может быть достаточно много, а нужны только те, в которых указаны наши параметры, то воспользуемся методом СоздатьВыражениеXPath объекта ДокументDOM (XPath по сути язык запросов к файлам xml).

Resolver = New DOMNamespaceResolver(DomDocument);

For Each MapElement In FillData Do
		
	XPathExpression = DomDocument.CreateXPathExpression("//w:t[contains(text(),'"+MapElement.Key+"')]", Resolver);
	XPathResult 	= XPathExpression.Evaluate(DomDocument);
		
	Node = XPathResult.IterateNext();
	While Node <> Undefined Do

		Node.TextContent = StrReplace(Node.TextContent, MapElement.Key, MapElement.Value);	
		Node = XPathResult.IterateNext();
			
	EndDo;
	
EndDo;

Поясню приведенный сниппет. Сначала создаем РазыменовательПространствИменDOM (подробнее  тут). Дальше обходим некую коллекцию данных, ключ которой это имя параметра, который подлежит замене, а значение - то, что должно быть вместо параметра. В метод СоздатьВыражениеXPath передаем запрос к xml, которым выберем все теги <w:t> файла на всех уровнях, текст которых содержит наш параметр. Вычисляем результат и обходим его, заменяя параметры нужными значениями.

Теперь сохраним ДокументDOM. Для этого используем объект ЗаписьDOM.

DOMWriter = New DOMWriter; 
XMLWriter = New XMLWriter;
XMLWriter.OpenFile(PathXMLFile); 
DOMWriter.Write(DomDocument, XMLWriter); 
XMLWriter.Close();

Осталось создать такой же контейнер, который мы распаковывали. Используем для этого ЗаписьZipФайла и уже знакомый объект ПотокВПамяти. Иерархия файлов в контейнере должна быть такой же, как и до распаковки. Для этого в метод Добавить объекта ЗаписьZipФайла передадим РежимСохраненияПутейZIP.СохранятьОтносительныеПути.

Stream = New MemoryStream;
	
ZipWriter = New ZipFileWriter(Stream,,,ZIPCompressionMethod.Deflate);
ZipWriter.Add(TempDir+"\*.*", ZIPStorePathMode.StoreRelativePath, ZIPSubDirProcessingMode.ProcessRecursively); 
ZipWriter.Write();
	
BinaryData = Stream.CloseAndGetBinaryData();

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

Скриншот полученной обработки

Результат работы заполнения документа Word. Шаблон до заполнения

Шаблон после заполнения

Работы проводились с Word 2010, но думаю, что на других версиях также должно работать.

Требования к платформе: версия 8.3.9+, так как  только в этой версии появилась возможность работы с потоками. Но эту проблему легко обойти, если использовать старый добрый ввод вывод через временные файлы.

Решение кроссплатформенное. В приложенных файлах обработка, код которой на 90% приведен в публикации.

 

74

Скачать файлы

Наименование Файл Версия Размер
FillDocx
.epf 32,45Kb
08.07.18
8
.epf 32,45Kb 8 Скачать

См. также

Комментарии
Сортировка: Древо
1. androgin 08.07.18 22:18 Сейчас в теме
код на англ., конечно красивый, но тут он гораздо более понятен на русском )
A1ice1990; PLAstic; CyberCerber; Дмитрий74Чел; unichkin; +5 Ответить
2. Артано 624 09.07.18 02:21 Сейчас в теме
(1) Если используются англоязычные объект или библиотека, то зачем уподобляться изобретателям ЕстьNull и городить двуязычный код?
alest; Lok`Tar; A_Max; +3 1 Ответить
3. nodel0 25 09.07.18 04:04 Сейчас в теме
разбор docx без применения COM хорошо реализован в документообороте, можно там подсмотреть реализацию.
theEmperor; PLAstic; user774630; fr13; Bassgood; +5 Ответить
13. fr13 09.07.18 17:24 Сейчас в теме
(3) (11) возможно. Может быть в рег. задании Извлечение текста что-то подобное используется. Не смотрел
4. rpgshnik 822 09.07.18 06:37 Сейчас в теме
Нужно попробовать, а то был геморрой с сохранением стилей в шаблонах. Особенно проблем не было с подставлением в док, а в докх стиль сбрасывался(((
5. AlX0id 09.07.18 08:14 Сейчас в теме
Я для подставления параметров использовал, собственно, поля Word:
1. Добавил в шаблон пользовательское поле, в значение прописал что-то типа в8_НомерДоговора.
2. В файлике docProps\custom.xml просто заменил этот текст на значение параметра.

Этот файлик на порядок более структурирован, нежели document.xml, да и поменьше будет. С построителем DOM было бы красивее наверн, но лень же )
PLAstic; kare; +2 Ответить
6. AlX0id 09.07.18 08:15 Сейчас в теме
А вот с таблицами все значительно интереснее.. Приходится стараться не касаться их пятиметровой палкой..
7. Поручик 4124 09.07.18 08:16 Сейчас в теме
Похожий парсер работает в нашей конфигурации с 2013 года.
8. qazaas 09.07.18 12:28 Сейчас в теме
For Each MapElement In FillData Do

XPathExpression = DomDocument.CreateXPathExpression("//w:t[contains(text(),'"+MapElement.Key+"')]", Resolver);
XPathResult = XPathExpression.Evaluate(DomDocument);

Node = XPathResult.IterateNext();
While Node <> Undefined Do

Node.TextContent = StrReplace(Node.TextContent, MapElement.Key, MapElement.Value);
Node = XPathResult.IterateNext();

EndDo;

EndDo;
Показать


Не понял, как вы получаете коллекцию "FillData"...
9. fr13 09.07.18 13:33 Сейчас в теме
(8) Это псевдо коллекция. В обработке заполняется фейковыми данными
10. Сурикат 178 09.07.18 14:43 Сейчас в теме
а у вас не было проблем с XPath,когда в тегах документа были указаны пространства имен?
12. fr13 09.07.18 17:22 Сейчас в теме
(10) была, но не в рамках этой задачи. В том файле была возможность это регулировать и я избавился от этого. По-другому так и не смог совладать. Было правда эта проблема года 1,5 назад. Может сейчас что-то и изменилось
11. morohon 09.07.18 16:43 Сейчас в теме
Насколько я помню в БСП 3.0 реализована работа с doc через XML
14. nytlenc 265 10.07.18 09:28 Сейчас в теме
ТабДок = Новый ТабличныйДокумент;
ТабДок.Прочитать(ИмяФайла, СпособЧтенияЗначенийТабличногоДокумента.Значение);
	
// Делаем что нужно
	
ТабДок.Записать(ИмяФайла, ТипФайлаТабличногоДокумента.DOCX);
15. user683100_ea.gladysheva 10.07.18 11:05 Сейчас в теме
Спасибо! Мне ваша публикация понравилась и в будующем пригодится
16. A1ice1990 110 10.07.18 18:10 Сейчас в теме
В мемориз только потому, что не знал, что docx суть архив с xml.
Этож все в корне меняет.

Ух теперь заживем.
17. wbazil 122 11.07.18 08:48 Сейчас в теме
https://infostart.ru/public/237032/
что то похожее тоже без Com но на русском
18. nilabs 6 11.07.18 13:00 Сейчас в теме
малореально, если шаблон будут править юзеры.
MS WORD 2013
Прикрепленные файлы:
19. nilabs 6 11.07.18 13:03 Сейчас в теме
(18) что кстати в https://infostart.ru/public/237032/ в комментариях и пишут
20. Stim213 342 16.07.18 15:09 Сейчас в теме
Сохранил вордовский документ-шаблон в файл xml
ПутьКФайлу =  "C:\счета клиентов\шаблон.xml";


на сервере его читаю в текстовый док

ТекстДок = Новый ТекстовыйДокумент;
ТекстДок.Прочитать(ПутьКФайлу,кодировкатекста.UTF8);
текст = ТекстДок.ПолучитьТекст();


подставляю значения параметров

текст = стрЗаменить(текст,"$POSTADRESS",ПочтовыйАдрес);


сохраняю файл doc
текстДок2 = Новый текстовыйДокумент;
текстДок2.УстановитьТекст(текст);
текстДок2.Записать(ИмяФайлаДоговора,кодировкатекста.UTF8);



и все это без всяких заморочек с ком-объектами и пр.
Skin123; Трактор; +2 Ответить
Оставьте свое сообщение