Word and sql server

Сейчас мы с Вами рассмотрим пример реализации того, как можно выполнить слияние данных Word с данными Microsoft SQL Server, при этом данный процесс будет автоматизирован средствами VBA Access 2003.

Слияние Word с SQL Server

Многие, наверное, уже умеют осуществлять слияние документов Word, например, с источником данных Excel или с тем же SQL сервером, но не все знают, как можно автоматизировать данный процесс или внедрить его в какую-нибудь программу.

Однажды у меня встала задача автоматизировать слияние некого шаблона Word с данными расположенными на SQL сервере, при этом все это необходимо было внедрить в программу, разработанную в Access 2003 (ADP проект). И сегодня я покажу пример решения данной задачи.

Содержание

  1. Исходные данные
  2. Создаем файл подключения к источнику данных MS SQL Server
  3. Создаем шаблон Word для слияния
  4. Код VBA Access 2003 для слияния документа Word с источником данных MS SQL Server

Исходные данные

И для начала давайте разберем исходные данные, т.е. что мы имеем.

Итак, в качестве клиента, как я уже сказал, у нас будет выступать ADP проект Access 2003. В качестве источника данных для примера будет выступать SQL Server 2012 Express. На компьютере установлен Microsoft Office 2013 (и Access 2003).

Создадим на сервере тестовую таблицу и заполним ее данными (допустим в базе данных Test). Для этого Вы можете запустить следующую SQL инструкцию.

Заметка! Начинающим программистам рекомендую почитать книгу «SQL код», с помощью которой Вы научитесь работать с языком SQL во всех популярных системах управления базами данных.

   
   CREATE TABLE dbo.TestTable(
        ID INT IDENTITY(1,1) NOT NULL,
        ProductName VARCHAR(50) NOT NULL,
        Price MONEY NULL,
   CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED (ID ASC)
   )
   GO
   INSERT INTO dbo.TestTable(ProductName, Price)
     VALUES ('Компьютер', 500)
   GO
   INSERT INTO dbo.TestTable(ProductName, Price)
     VALUES ('Монитор', 400)
   GO
   INSERT INTO dbo.TestTable(ProductName, Price)
     VALUES ('Телефон', 200)
   GO
   INSERT INTO dbo.TestTable(ProductName, Price)
     VALUES ('Планшет', 300)
   GO      
   INSERT INTO dbo.TestTable(ProductName, Price)
     VALUES ('Принтер', 250)
   GO
   SELECT * FROM TestTable

Скриншот 1

Заметка! Если Вы не знаете, что делает вышеуказанная инструкция, рекомендую посмотреть мой видеокурс «T-SQL. Путь программиста от новичка к профессионалу. Уровень 1 – Новичок», который предназначен для начинающих. В нем подробно рассмотрены все базовые конструкции языка SQL, включая все вышеперечисленные.

Теперь давайте создадим файл подключения (ODC) к нашему источнику данных. Данный файл будет выступать своего рода «шаблоном файла подключения», так как впоследствии мы можем, и будем переопределять и базу данных и сам SQL запрос.

Для создания файла подключения к SQL серверу давайте откроем Word и стандартным способом создадим данный файл, т.е. с помощью функционала «Рассылки». (Кстати пример создания подключения к SQL серверу из Excel мы с Вами уже рассматривали в материале – Excel — Подключение и получение данных с SQL сервера).

Переходим на вкладку рассылки и в меню «Выбрать получателей» выбираем «Использовать существующий список».

Скриншот 2

Затем в окне выбора источника данных нажимаем кнопку «Создать».

Скриншот 3

Далее выбираем тип источника данных, т.е. Microsoft SQL Server. Жмем «Далее».

Скриншот 4

Курс по SQL для начинающих

Потом вводим адрес сервера и нажимаем «Далее».

Скриншот 5

Затем выбираем базу данных и таблицу для подключения, еще раз напомню, это всего лишь шаблон, все эти параметры мы будем переопределять, жмем «Далее».

Скриншот 6

И в заключение вводим понятное название файла подключения, а также мы можем сразу его сохранить в нужный нам каталог путем кнопки «Обзор», по умолчанию он сохраняется в «C:UsersИмя_ПользователяDocumentsМои источники данных». Нажимаем «Готово».

Скриншот 7

Все, файл создан, Word можем закрыть без сохранения.

Создаем шаблон Word для слияния

Теперь давайте подготовим шаблон Word, т.е. это тот документ, в который мы будем подставлять данные из базы данных SQL сервера.

Вся подготовка сводится к тому, что нам необходимо вставить поля слияния там, где это нам нужно. Это делается следующим образом. Вкладка «Вставка -> Экспресс-блоки -> Поле».

Скриншот 8

Ищем поле MERGEFIELD и вводим название поля, которое будет соответствовать полю в источнике данных (в моем случае это ProductName и Price). Жмем «ОК».

Скриншот 9

Так как у меня это тестовый шаблон в нем текста практически не будет, и выводить я буду всего два поля, у Вас скорей всего будет много текста и много полей слияния.

Скриншот 10

Код VBA Access 2003 для слияния документа Word с источником данных MS SQL Server

Осталось написать код VBA, который будет осуществлять слияние. Для примера давайте добавим на форму кнопку StartMerge и поле Price для фильтрации данных. Затем в редакторе Visual Basic напишем процедуру для слияния, допустим с названием MergeWord, и в обработчик события кнопки StartMerge (нажатие кнопки) вставляем код вызова этой процедуры. Весь код будет выглядеть следующим образом (я его прокомментировал). Сразу поясню, что шаблон Word и файл ODC у меня лежат в каталоге D:Test.

   
   'Процедура для запуска слияния
   Private Sub MergeWord(TemplateWord As String, QuerySQL As String)
   'Первый параметр - Путь к шаблону Word
   'Второй параметр - Строка запроса к БД
   On Error GoTo Err1
   Dim ConnectString As String, PathOdc As String
   Dim WordApp As Object
   Dim WordDoc As Object
   'Шаблон файла ODC для подключения к данным
   PathOdc = "D:TestTestSourceData.odc"
   If TemplateWord <> "" Then
    'Создаем документ Word
    Set WordDoc = CreateObject("Word.document")
    Set WordDoc = GetObject(TemplateWord)
    Set WordApp = WordDoc.Parent
    'Создаём подключение к источнику данных (MS SQL Server)
    'Некоторые данные берём из текущего подключения ADP проекта
    ConnectString="Provider=SQLOLEDB.1;  " & _
                  "Integrated Security=SSPI;" & _
                  "Persist Security Info=True; " & _
                  "Initial Catalog=" & CurrentProject.Connection.Properties("Initial Catalog") & "; " & _
                  "Data Source=" & CurrentProject.Connection.Properties("Data Source") & "; " & _
                  "Use Procedure for Prepare=1;" & _
                  "Auto Translate=True;" & _
                  "Packet Size=4096;" & _
                  "Use Encryption for Data=False;"
    'Задаем источник данных
    WordDoc.MailMerge.OpenDataSource NAME:=PathOdc, _
                                     Connection:=ConnectString, _
                                     SQLStatement:=QuerySQL
    'Делаем видимым Word
    WordApp.Visible = True
    WordApp.Activate
    'Начинаем слияние
    With WordDoc.MailMerge
                .Destination = wdSendToNewDocument
                .SuppressBlankLines = True
                .Execute Pause:=False
    End With
    'Закрываем шаблон без сохранения
    WordDoc.close (wddonotsavechanges)
    Set WordDoc = Nothing
    Set WordApp = Nothing
   Else
    MsgBox "Не указан шаблон для слияния", vbCritical, "Ошибка"
   End If
   Ex1:
    Exit Sub
   Err1:
    MsgBox Err.Description
    WordDoc.close (wddonotsavechanges)
    WordApp.Quit
    Set WordDoc = Nothing
    Set WordApp = Nothing
    Resume Ex1
   End Sub

   Private Sub StartMerge_Click()
   Dim Filter As String
   Filter = ""
   'Условие
   If Nz(Me.Price, "") <> "" Then
    Filter = "WHERE Price >= " & Me.Price
   End If
   'Вызов процедуры слияния
   Call MergeWord("D:TestШаблон.docx", "SELECT * FROM ""TestTable"" " & Filter & " ")
   End Sub

Сохраняем и проверяем работу.

После нажатия на кнопку (StartMerge) запустится Word, в котором уже все данные заполнены и документов будет столько, сколько строк в источнике.

Скриншот 11

Как видим, все работает. На этом у меня все, надеюсь, материал был полезен. Пока!

Never a man to walk away from a challenge, Phil Factor set himself the task of automating the production of Word reports from SQL Server, armed only with OLE automation and a couple of stored procedures.

Having helped Robyn with her Excel Workbench, I couldn’t get out of my head the idea of achieving the same effect with MS Word. After all, from the data viewpoint, MS Word documents are just a series of paragraphs and tables aren’t they? Surely, it should be easy to read and write data between SQL Server and Word.

Robyn backed away nervously at this point, to leave me full rein. There are reams of advice on why it was a very bad idea to do such a thing as read data from Word documents into SQL, and plenty of indignation at the idea of writing to Word. I always smell humbug when I hear this sort of talk. If Microsoft fail to do something properly with a product, one gets quasi-religious pronouncements from everywhere that it is not appropriate to use the product in this way, or it traduces one’s architectural design, or it should be done in C #.

Nevertheless, I quickly realized that there were plenty of obstacles in my way. For a start, I was puzzled by the lack of ODBC drivers for Word. You’d have thought that all one has to do is to indicate which table in the document you want to populate, and send it the result of the SQL. You’d be able to read the document attributes as if it was a built-in table and the contents of the paragraphs as if it were another.

The next indication I had that something was really amiss was when I started trying a few simple automation tasks with Word, using OLE Automation in TSQL. Whenever a mistake happened, a warning sound came through the speakers of the server, scaring the life out of a dozing DBA. It looked as if Word was never intended for such connectivity. This was strange, as Excel is so well-mannered in this respect, a tame creature that handles errors obligingly and does exactly what is said in the documentation. I was not emotionally prepared for recalcitrance on the scale I was confronted with.

Some perfectly reasonable OLE methods, taken from fully operational VBA examples, never worked. I waded through reams of example scripts, noting with some alarm the trepidation expressed by the authors of scripts on Technet (‘Two tables in a document via automation? Dear me’, and ‘we employed a stunt double to do the testing’). I could find almost no successful attempts at OLE automation via TSQL. I just had to design around them. I came close to throwing in the towel, but my stubborn streak took over. Dammit, I was going to succeed.

And here, at last, are two stored procedures, too long to show in the body of this article, but attached nevertheless:

  • SpWord_Document_content – extracts the text from an MS Word file into a table of paragraphs, along with the name of the style of the original paragraph
  • spExportToWord – creates a new Word file, and writes results from the SQL expression to a table in Word, with a heading if required.

If you want to follow along and test them out, download them now from the CODE DOWNLOAD link above, or from the direct text file links below.

Reading from Word paragraphs to SQL Server

This SpWord_Document_content T-SQL stored procedure is easy to use. It is a pretty straightforward automation of Word, though I’ve seen no stored procedure like it anywhere. Remember that you will need to have MS Word installed on the server you are executing the stored procedure from.

drop table #MyContentCreate table #MyContent (   Paragraph_ID int,   Style varchar(20),   Content Varchar(8000))Insert into #myContent   Exec spWord_Document_content @documentFile =‘c:FooBar.doc’

In case you are wondering whether it reads any tables and other content that happens to be in the way; it doesn’t. It only reads the words of the paragraphs. The difficulties involved in providing a generic Word reader multiply at the point when one considers tables, and it is far easier to automate the saving of the file as text, and then pulling into SQL Server via the automation of Windows.FileSystemObject.

Exporting from SQL Server to Word

The spExportToWord T-SQL stored procedure creates a new Word file, and writes results to a Word table in that file. This allows you to autoformat the results in any of the standard ways that Word allows.

I decided to use ADODB, mainly in order to get the field-names of any result. However, the other advantage was that I could use it to connect to servers anywhere (I administer database-driven websites) without permanent links.

NOTE:
I also had the ambitious idea of allowing you to write several tables, using multiple recordsets from the one query. Sorry. Next time maybe

All you need to run this is to have Word installed on your server, and have configured SQL Server to allow OLE Automation. If you have a trusted connection, and the query is in your current database, then the syntax is simple:

Execute spExportToword @QueryText=‘MyQuery’,   @documentFile=‘C:foobar.doc’,@Tableformat=‘Colourful 1’

Or, if you have SQL Server security then you need to specify the password:

Execute spExportToword @QueryText=‘MyQuery’,   @SourceUID =‘MyUserID’, @SourcePWD=‘MyPassword’   @documentFile=‘C:foobar.doc’,   @Tableformat=‘Colourful 1’

If you don’t like the ‘professional’ auto format that I chose by default (‘colourful 1’ is just one of the many built-in auto formats you can see in Word), then try this:

create table sample(   [ ] varchar(80),   [Software Sales] varchar(80),   [Hardware Sales] varchar(80),   [Consultancy] varchar(80),   [Total] varchar(80))insert into sample

   select ‘First Quarter’,‘£1940’,‘£567’,‘£765’,‘£3272’insert into sampleselect ‘Second Quarter’,‘£15960’,‘£3685’,‘£34000’,‘£53645’insert into sample    select ‘Third Quarter’,‘£39480’,‘£5000’,‘£23000’,‘£67480’insert into sample    select ‘Fourth Quarter’,‘£23960’,‘£3549’,‘£3470’,‘£30979’insert into sample select ‘Total’,‘£81340’,‘£12801’,‘£61235’,‘£155376’execute spExportToword @QueryText=‘Select * from sample’,   @documentFile=‘C:report.doc’,@Tableformat=‘Grid 6’

This should result in the following table:

 

Software Sales

Hardware Sales

Consultancy

Total

First Quarter

£1940

£567

£765

£3272

Second Quarter

£15960

£3685

£34000

£53645

Third Quarter

£39480

£5000

£23000

£67480

Fourth Quarter

£23960

£3549

£3470

£30979

Total

£81340

£12801

£61235

£155376

You can also provide a heading to the table to explain what the data is about.

execute spExportToword @QueryText=‘Select * from sample’,   @documentFile=‘C:report.doc’,@Tableformat=‘Grid 6’,   @tableHeading=‘2006 Earnings. The Kamakaze Laxative Company’

If you have a slightly more complex access requirement then you can specify the server, database, userid and password

spExportToword @Sourceserver=‘MyServer’,@SourceDatabase=‘MyDatabase’,@SourceUID=‘MyUserID’,@SourcePWD=‘MyPassword’,@QueryText=‘Select * from MyTable’,   @documentFile=‘C:MyDocument.doc’

You can, of course, get serious about your connection string to get a report from a server managing a distant ecommerce site.

spExportToword @QueryText=‘spWho’,   @documentFile=‘C:remotestuff.doc’,   @ConnectionString=‘Driver={SQL Server};                      Server=xxx.xxx.xxx.xxx;                      Address=xxx.xxx.xxx.xxx,1433;                      Network=DBMSSOCN;                      Database=myDatabaseName;                      Uid=myUsername;                      Pwd=myPassword’

Or, if you wish, you can connect to any ODBC source you can think of! The secret is in the connection string.

A few words of caution

Something like this, which is perfectly serviceable for small-scale applications, may not necessarily scale-up – please bear that in mind if you are considering putting this sort of code into a production server.

Whereas Excel is relatively well-behaved, MS Word is notorious for hitting a bug, forgetting that it is being automated as a background task without a user-interface, and popping up a system modal dialog box.

Also, bear in mind that OLE Automation has to be done by a process with extensive login roles. You can’t give these sorts of roles to the ordinary user. It is much better to queue up jobs like this and have a scheduled process on SQL Server Agent to execute them, using a login with the correct roles.

Summary

There is much more that can be done, of course. It would not be a big problem to read from Word tables, and I’d have done it for you if I’d been able to think of a useful application. It would also be reasonably easy to write out a mixture of tables and paragraphs into a word file too. It all depends on the requirements of your application. Hopefully, armed with the code I include, it may give you the impetus to try things out.

However get in plenty of sandwiches and Jack Daniels before doing so, as this sort of task is not for the faint-hearted.

Это сообщение будет посвящено экспорту данных из файла Word средствами SQL Server-а. Мы напишем хранимую процедуру, которая будет получать в качестве параметра полное имя doc-файла и номер таблицы в нем. Предполагается что таблицы в документе нумеруются с 1 в том порядке, в каком они появляются в файле. Процедура должна будет вернуть содержимое таблицы документа в виде таблицы сервера.


С помощью такой процедуры можно, например возпользоваться конструкцией insert into … exec … для вставки содержимого Word-таблицы в таблицу SQL-сервера. Предполагается, что на серверном компьютере установлен Word, и для работы с Word-ом будут использоваться COM-объекты.


Для работы с объектами будут использоваться процедуры sp_OA… Конечно, более эффективным и безопасным решением могло бы стать использование clr-сборок. Да и для работы с word-ом могут быть библиотеки, работающие напрямую с файлом без OLE. Я этого не проверял, и решил воспользоваться процедурами OLE из любви к экзотическим задачам. К тому же решение может пригодиться для сопровождения унаследованных серверов, версии которых не позволяют использовать более современные средства.


Перед написанием кода процедуры сделаем несколько предварительных замечаний. Когда процедуры sp_OACreate или sp_OAMethod создают объект, то в конце он обязательно должен быть уничтожен с помощью вызова процедуры sp_OADestroy. К тому же понадобится открывать Word-файл. В конце файл обязательно надо будет закрыть, вызывая метод Close. Также необходимо будет вызвать метод Quit для приложения Word, чтобы в процессах не остался висеть процесс WINWORD. В связи с этим, на случай ошибок в работе процедуры, весь код по работе с объектами будет помещен в блок try блока try/catch. А инструкции для закрытия приложений и уничтожения объектов будут располагаться после блока catch, чтобы они были гарантированно вызваны. Это сделает код более безопасным.


Есть еще одно важное замечание, связанное с доступами. Для того, чтобы можно было работать с документами на стороне сервера, на диске, на котором установлена операционная система, должна существовать специальная директория. Для 64-х битных операционных систем это директория C:WindowsSysWOW64configsystemprofileDesktop. Для 32-х битных это директория C:WindowsSystem32configsystemprofileDesktop. По умолчапнию, в новых версия Windows, этой директории может не быть. В этом случае ее надо создать и предоставить права на работу с ней учетной записи, под которой работает служба SQL Server-а. Если этого не сделать, то применение метода Open будет завершаться ошибкой, поскольку сервер не будет видеть файл, открываемый с помощью sp_OAMethod.


Теперь можно переходить в коду. Первая часть кода стандартна для таких задач. Сперва запускается Word, затем открывается файл, в фале находится коллекция таблиц. После чего создается объект таблица (она отыскивается по номеру элемента в коллекции). Затем с помощью свойств Rows.Count, Columns.Count можно получить число строк и столбцов таблицы. В упрощенном виде код выглядит так:
set @oper = N’создание объекта’

exec @res = sp_OACreate N’Word.Application’, @App out

set @han = @App if @res <> 0 raiserror ( @oper, 16, 1 )



set @oper = N’открытие файла’

exec @res = sp_OAMethod @App, ‘Application.Documents.Open’, @Doc out, @DocName

set @han = @App if @res <> 0 raiserror ( @oper, 16, 1 )



set @oper = N’коллекция таблиц’

exec @res = sp_OAMethod @Doc, N’Tables’, @TabCol out

set @han = @Doc if @res <> 0 raiserror ( @oper, 16, 1 )



set @oper = N’таблица’

exec @res = sp_OAMethod @TabCol, N’Item’, @Tab out, @TabNum

set @han = @TabCol if @res <> 0 raiserror ( @oper, 16, 1 )



set @oper = N’строки’

exec @res = sp_OAGetProperty @Tab, N’Rows.Count’, @Rows out

set @han = @Tab if @res <> 0 raiserror ( @oper, 16, 1 )



set @oper = N’столбцы’

exec @res = sp_OAGetProperty @Tab, N’Columns.Count’, @Cols out

set @han = @Tab if @res <> 0 raiserror ( @oper, 16, 1 )



Эта часть кода, которая будет идти в самом начале блока try. Далее возникла мысль о том, что можно идти в цикле по строкам и ячейкам таблицы, складывая значения ячеек во временную таблицу. Например так:
while @curRow <= @Rows

begin

    set @curCol = 1

    while @curCol <= @Cols

    begin

        set @cmd = N’Cell(‘ + cast ( @curRow as varchar ( 10 ) ) + ‘,’ + cast ( @curCol as varchar ( 10 ) ) + ‘).range.Text’

        exec @res = sp_OAGetProperty @Tab, @cmd, @CurCell out

        if @res <> 0 raiserror ( @oper, 16, 1 )

        set @CurCell = rtrim ( ltrim ( @CurCell ) )

        if unicode ( right ( @CurCell, 1 ) ) = 7

        begin

            set @CurCell = substring ( @CurCell, 1, len ( @CurCell ) 1 )

        end

        if unicode ( right ( @CurCell, 1 ) ) = 13

        begin

            set @CurCell = substring ( @CurCell, 1, len ( @CurCell ) 1 )

        end

        if @curCol = 1

        begin

            set @sql = N’insert into ‘ + @TabName + N’ ( col1 ) select N»’ + @CurCell
       
end

        else

        begin

            set @sql = N’update ‘ + @TabName + N’ set col’ + cast ( @curCol as nvarchar ( 100 ) ) + ‘ = N»’ + @CurCell + N»’

            where iRowId = ‘ + cast ( @curRow as nvarchar ( 100 ) )
        end

        exec sp_executesql @sql

        set @curCol += 1

    end

    set @curRow += 1

end



В этом этом коде предполагается, что предварительно создана временная таблица, структура которой совпадает с таблицей Word-а. Ее можно создать, используя динамичекий sql и информацию о числе столбцов в Word-таблице. В цикле, на каждой итерации, в таблицу происходит либо вставка, в случае, если обрабатывается новая строка таблицы. Либо идет обновление столбца одной строки таблицы, если работает внутренний цикл, который считывает столбцы текущей строки таблицы документа.


Однако при использовании этого подхода возникла проблема. Оказалась, что на больших объемах, когда в таблице тысячи или десятки тысяч строк, код работает чудовищно долго. Можно было бы грешить на то, что на каждой итерации работает логируемая операция insert или update, для которой к тому же, возможно, всякий раз ищется план выполнения из-за использования Ad Hoc-запросов. Но профайлер показал, что это время является мизерным по сравнению с общим. Основное время затрачивается на считывание очередной ячейки. Потребовалось изменить подход. В объектной модели Word-а я нашел метод ConvertToText. Он оказался исключительно полезным. Метод выполняет конкатенацию всех строк и столбцов таблицы документа, возвращая результирующую строку. В качестве разделителя строк используется новая строка. А разделитель столбцов можно задать в параметре метода (по умолчанию символ тире («-«)). Этот метод работает радикально быстрее чем цикл по всем ячейкам.


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


Первое, что потребуется сделать, это создать временную таблицу с одним столбцом типа nvarchar ( max ), и положить в нее строку. Далее с помощью программы bcp можно скопировать содержимое таблицы в текстовый файл. Операция нелогируемая и очень быстрая. Далее, используя информацию о разделителях строк и столбцов, можно выполнить загрузку файла в таблицу с помощью инструкции bulk insert. Инструкция также нелогируемая и работает чрезвычайно быстро.


По алгоритму вроде бы все. То есть почти все. После применения метода ConvertToText с помощью sp_OAMethod мы получаем объект типа Range. Для получения результата конкатенации требуется использовать метод Text через вызов процедуры sp_OAMethod с созданным объектом типа Range в качестве параметра. Метод имеет выходной параметр, который надо поймать в третьем параметре процедуры sp_OAMethod. Это делается так:
exec @res = sp_OAMethod @Tab, N’ConvertToText’, @ran out, ‘;’
exec @res = sp_OAMethod @ran, N’Text’, @txt out

Однако у процедуры sp_OAMethod есть одна не очень приятная особенность. Если длина выходной строки будет больше 8000 символов, то прочитать значение не получится. В старых версия SQL Server-а просто не было типов данных varchar ( max ) / nvarchar ( max ), и поэтому третий параметр не может принадлежать таким типам. Выход такой. Требуется вызвать sp_OAMethod, указывая, что выходной параметр не нужен. То есть указать в третьем параметре null. Тогда вызов процедуры сам вернет результат метода, и его можно сохранить используя конструкцию insert into … exec … Пример:

insert into #tmp ( val )


    exec @res = sp_OAMethod @ran, N’Text’, null

Ниже представлен общий код процедуры. После него будут даны комментарии к коду и результаты сравнительного тестирования различных методов на скорость обработки больших таблиц.


if sessionproperty ( N’quoted_identifier’ ) = 0

    set quoted_identifier on
go

if sessionproperty ( N’ansi_nulls’ ) = 0

    set ansi_nulls on
go

if object_id ( N’dbo.ExportWordTableUsingConvertToTxt’, N’P’ ) is null
    exec ( N’create proc dbo.ExportWordTableUsingConvertToTxt as return 1′ )
go

alter
proc dbo.ExportWordTableUsingConvertToTxt

(

       @DocName     nvarchar ( 300 ),

       @TabNum             int
= 1

)

as

begin

       set nocount, xact_abort on

       declare @App int, @Doc int, @res int, @src nvarchar ( 300 ), @desc nvarchar ( 300 ), @oper nvarchar ( 100 ), @ErrMsg nvarchar ( max ),


             @han
int, @TabCol int, @Tab int, @Rows int, @Cols int, @CurCell varchar ( 100 ), @curCol int, @curRow int, @cmd varchar ( 8000 ),

             @sql
nvarchar ( max ), @TabName nvarchar ( 100 ), @CurColName int, @txt nvarchar ( max ), @ran int,

             @FileName
varchar ( 255 ) = ‘C:UsersedynakDesktoptmp.txt’

 
       begin try

             set
@oper = N’
создание объекта

             exec
@res = sp_OACreate N’Word.Application’, @App out

             set
@han = @App if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
открытие файла

             exec
@res = sp_OAMethod @App, ‘Application.Documents.Open’,
@Doc out, @DocName

             set
@han = @App if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
коллекция таблиц

             exec
@res = sp_OAMethod @Doc, N’Tables’, @TabCol out

             set
@han = @Doc if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
таблица

             exec
@res = sp_OAMethod @TabCol, N’Item’, @Tab out, @TabNum

             set
@han = @TabCol if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
строки

             exec
@res = sp_OAGetProperty @Tab, N’Rows.Count’, @Rows out

             set
@han = @Tab if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
столбцы

             exec
@res = sp_OAGetProperty @Tab, N’Columns.Count’, @Cols out

             set
@han = @Tab if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
конвертация таблицы

             exec
@res = sp_OAMethod @Tab, N’ConvertToText’, @ran out, ‘;’

             set
@han = @Tab if @res <> 0 raiserror ( @oper, 16, 1 )

             set
@oper = N’
тест таблицы

             if object_id ( N’tempdb..#tmp’, N’U’ ) is not null

                    drop
table #tmp

             create
table #tmp ( val varchar ( max ) null )

             insert
into #tmp ( val )

                    exec
@res = sp_OAMethod @ran, N’Text’, null

             set
@han = @ran if @res <> 0 raiserror ( @oper, 16, 1 )    

            
             select
top 1 @txt = val

             from
#tmp



             set
@oper = N’
ячейка

             set
@curCol = 1

             set
@curRow = 1

             set
@CurColName = 1

             set
@sql =


             while
@CurColName <=
@Cols


             begin

                    set
@sql = @sql + case when @CurColName = 1 then else N’, ‘ end +

                           N’col’ + cast ( @CurColName as nvarchar ( 100 ) ) + N’ nvarchar ( 100 ) null’

                    set
@CurColName +=
1

             end


             set
@TabName = quotename ( N’##’ + cast ( newid () as nvarchar ( 100 ) ) )


             set
@sql = N’

                    if object_id ( N»tempdb..’
+ @TabName + »’, N»U» ) is not null

                           drop table ‘
+ @TabName +

                    create table ‘
+ @TabName +

                    (

                          
+ @sql +

                    ) on [PRIMARY]

            

             exec
sp_executesql @sql


             declare
@TmpGlobTab nvarchar ( max ) = quotename ( N’##’ + cast ( newid () as nvarchar ( 100 ) ) )


             set
@sql = N’

                    if object_id ( N»tempdb..’
+ @TmpGlobTab + »’, N»U» ) is not null

                           drop table ‘
+ @TmpGlobTab +

                    create table ‘
+ @TmpGlobTab + ‘ ( val varchar ( max )
null )

            

             exec
sp_executesql @sql


             set
@sql = N’


                    insert into ‘
+ @TmpGlobTab + ‘ ( val )

                           select replace ( val,
nchar ( 13 ), N»+» )

                           from #tmp

                    update ‘
+ @TmpGlobTab +

                           set val = left ( val,
len ( val ) — 1 )

            

             exec
sp_executesql @sql


             set
@cmd = ‘bcp «select * from ‘ +
@TmpGlobTab + ‘» queryout ‘ + @FileName + ‘ -S’ + @@servername + ‘ -T -c -C 1251 -r n’


             exec
xp_cmdshell @cmd, no_output


             set
@sql = N’


                    bulk insert ‘
+ @TabName +

                    from »’
+ @FileName + »’

                    with

                    (

                           firstrow            = 1,

                           codepage            = 1251,

                           datafiletype = N»char»,

                           batchsize           = 5000000,

                           rowterminator = N»+»,

                           fieldterminator =
N»;»

                    )

            

             exec
sp_executesql @sql


             set
@sql = N’


                    select *

                    from
+ @TabName +

            

             exec sp_executesql @sql

       end try

       begin catch

             if error_message () <> @oper

             begin

                    set @ErrMsg = N’Описание ошибки: ‘
+ error_message () + N’; номер: ‘ + cast ( error_number () as nvarchar ( 100 ) ) + N’; строка: ‘ +

                           cast ( error_line () as nvarchar ( 100 ) )

             end

             else

             begin

                    exec sp_OAGetErrorInfo @han, @src out, @desc out

                    set @ErrMsg = N’Тип ошибки: ‘ + @oper + N’; код: ‘ + cast ( @res as nvarchar ( 100 ) ) + ‘; источник: ‘ + isnull ( @src, N’неизвестен’ ) +

                           ‘; описание: ‘ + isnull ( @desc, N’неизвестен’ )

             end

       end catch


       — закрытие объектов, сборка мусора


       exec sp_OAMethod @Doc, ‘Close’, null, 0

       exec sp_OAMethod @App, ‘Quit’, null

       exec sp_OADestroy @ran

       exec sp_OADestroy @Tab

       exec sp_OADestroy @TabCol

       exec sp_OADestroy @Doc

       exec sp_OADestroy @App



       set @sql = N’

             if
object_id ( N»tempdb..’

+ @TabName + »’, N»U» ) is not null

                    drop
table ‘
+ @TabName +

      

       exec sp_executesql @sql


       set @sql = N’


             if
object_id ( N»tempdb..’

+ @TmpGlobTab + »’, N»U» ) is not null

                    drop
table ‘
+ @TmpGlobTab +

      

       exec sp_executesql @sql

       set @cmd = ‘del ‘ + @FileName

       exec xp_cmdshell @cmd, no_output

       if isnull ( @ErrMsg, ) <>

       begin

             raiserror ( @ErrMsg, 16, 1 )

       end

end

go



В коде выше запускается Word, открывается файл. После этого код получает возможность работы с таблицей. К ней применяется метод ConvertToText, результат которого записывается во временную таблицу #tmp. Такая таблица локальна и не видна другим соединениям. Поэтому далее создается глобальная временная таблица, в которую копируется содержимое #tmp. Промежуточная таблица #tmp, используется ввиду желания не задавать в коде имени глобальной временной таблицы, из-за чего с последней можно работать только с помощью динамического sql. На всякий случай, символы новой строки заменяются на символ +, на случай если пустые строки пропадут при работе с bcp. Также в самом конце строки есть символ новой строки, который преобразован в символ +. При bulk insert сервер подумал бы, что после этого последнего символа не хватает строки, из-за чего bulk insert выдал бы ошибку. Поэтому в update-е глобальной временной таблицы последний символ удаляется из строки. После копирования содержимого таблицы в текстовый файл, работает bulk insert, который использует информацию о разделителях столбцов и строк: символы плюса («+») и точки с запятой («;»).


В блоке catch логируется ошибка. Она может быть связана с кодом процедуры или с работой объектов. В зависимости от этого либо вызывается sp_OAGetErrorInfo либо используются стандартные функции логирования ошибки.


После блока catch производится сборка мусора. Обратите внимание, что метод Close для документа обязательно вызывается с паратром 0. Это гарантирует, что изменения внесенные кодом процедуры в документ не сохранятся. Это важно, ведь метод ConvertToText конвертирует таблицу в самом файле. Также с помощью процедуры sp_OADestroy уничтожаются объекты. Производится удаление созданных глобальных временных таблиц и на всякий случай удаляется созданный текстовый файл.


Результаты по скорости такие. Тестирование проводил на компьютере с ОС Win7 x64 (4 Gb оперативной памяти, один 4-х ядерный процессор). Сделал несколько Word-файлов, содержащих по таблице с 4-я столбцами. Все поля заполняются одинаковыми значениями.

В 1-ом файле в таблице 1000 строк. Процедура, выполяющая конвертацию таблицы в текст отработала за 1 сек. Процедура, делающая цикл по ячейкам работала 30 сек.
Для файла с таблицей, содержащей 10 тыс. строк конвертация отработала за 45 сек., с циклом по ячейкам время работы составило уже 22 мин. 10 сек.

Для файла с 50-ю тысячами строк процедура с конвератцией таблицы в текст работала 28 мин. 27 сек., процедура с циклом по ячейкам — 8 час. 23 мин. 29 сек..

И наконец, word-таблица со 100 тысячами строк импортировалась через конверацию 2 часа 36 мин. 21 сек. Процедура с циклом по ячейкам работала несколько суток, после чего соединения отключилось.

  • Remove From My Forums
  • Question

  • how to import a word document (including all the bullets and tables) into a sql server database

    thanks!

Answers

  • You can store word docuemnts into SQL Server Database

    You store those documents into a table with VARBINARY(MAX) Column

    You can either use

    a) Import Export Wizard

     see
    http://social.msdn.microsoft.com/forums/en-us/sqltools/thread/2686485E-AF60-4F35-A115-8E488AEE157F

    b) SSIS

    c) T-SQL

    -- Declare a variable to store the image data
    DECLARE @Doc AS VARBINARY(MAX)
    
    -- Load the image data
    SELECT @doc = CAST(bulkcolumn AS VARBINARY(MAX))
    FROM OPENROWSET( BULK 'C:DocumentsDocName.doc', SINGLE_BLOB ) AS x 
     
     
    INSERT INTO Documents (ID, Document)
    VALUES (1, @doc )
    
    

    d) Application (that can be developed in any supported programmign language like .net, Java, Visual Basic 6 etc.)

    e) If you are storing word docs bigger than 1 MB in size and using SQL 2008 or later you may want to enable filestream property of the varbinary(max) columns….

    • Marked as answer by

      Wednesday, December 22, 2010 8:35 PM

Время прочтения: 3 мин.

Отчёт – это особая форма представления данных. Он позволяет пользователю познакомиться с  отобранными, агрегированными и особо оформленными данными. На сегодняшний день формирование и представление отчетов все еще является актуальной формой доведения до пользователя информации.

В этой статье рассматривается один из способов автоматического формирования отчета в формате файла MS WORD, начиная от непосредственного запроса к данным БД MSSQL и заканчивая его оформлением. В качестве инструментария используется python и модуль docx.

Для начала ознакомимся с используемыми данными. В БД имеются 3 таблицы с данными о товарах, покупателях и продажах.

Для начала нам понадобится импортировать в код необходимые модули:

from docx import Document
import pandas as pd
import pandas.io.sql as psql
import matplotlib.pyplot as plt
from io import BytesIO
import pyodbc

Создаем соединение и формируем запрос. На данном этапе пользователю следует определиться с составом запрашиваемых запросом данных исходя из имеющейся задачи, т.е. на каком этапе производятся расчеты, агрегирование и/или фильтрация данных. В нашем случае будут запрашиваться данные всех продаж с привязкой к ФИО покупателя и данным о товаре. Обработка данных будет производится с помощью модуля pandas.

cnxn = pyodbc.connect("Driver={SQL Server Native Client 11.0};"
                        "Server=S1;"
                        "Database=test;"
                        "uid=sa;pwd=pass;"
                        "Trusted_Connection=yes;")
cursor = cnxn.cursor()

sql = '''select 
                 c.name
                ,p.*
                from [dbo].[sales] s

                join [dbo].[customers] c
                on c.id = s.customer

                join [dbo].[product] p
                on s.product = p.id'''

Полученные данные отправляем в dataframe, закрываем соединение

df = psql.read_sql_query(sql,cnxn)
cnxn.close()
del df['id']  # ненужный столбец df

Следующий этап – непосредственно создание документа.

document = Document() # создается объект

# добавляем первый заголовок
document.add_heading('Отчет о продажах', 0) 

# добавляем простой текст с переменными из 
# данных таблицы (названия магазинов)
shop_list = ', '.join(df['shop'].unique().tolist())

p = document.add_paragraph('Отчет о продажах в магазинах: ')

# к тексту добавим сам список, выделяем жирным шрифтом
p.add_run(shop_list).bold = True

Формируем таблицу о всех продажах – аналог входных данных запроса.

document.add_heading('Общие продажи', level=1) # заголовок

rows, columns = df.shape  # размеры dataframe
table = document.add_table(rows=1, cols=columns) # создаем таблицу
table.style = "Colorful List Accent 1" # определяем стиль

# формируем заголовки таблицы
hdr_cells = table.rows[0].cells
for i in range(columns):
    hdr_cells[i].text = list(df.columns.values)[i]

# заполняем данными из dataframe
for row in range(rows):
    row_cells = table.add_row().cells
    row_data = df.iloc[row].tolist()
    for column in range(columns):
        row_cells[column].text = str(row_data[column])

На выходе получается следующий документ.

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

# заголовок
document.add_heading(' ', 0)
document.add_heading('График общих затрат покупателей', 0) 

# пустой объект, куда будет помещен plot 
memfile = BytesIO()
fig = plt.figure()
# данные для графика: клиенты и суммы затрат
plt.plot(df.groupby('name')['price'].sum()) 
fig.savefig(memfile)
document.add_picture(memfile) # размещение в документе
document.save('demo.docx') # публикация в файловой системе

На выходе получаем.

Это далеко не все возможности модуля docx, позволяющие произвести верстку документа «на лету» с использованием данных, взятых непосредственно из БД и агрегированных с помощью Python. Более подробную информацию о верстке, использовании стилей, вставке объектов и т.п. можно ознакомится на сайте разработчиков https://python-docx.readthedocs.io.

Like this post? Please share to your friends:
  • Word and spanish accents
  • Word and pdf viewer
  • Word and spaces count
  • Word and pdf reader
  • Word and sound records