Сегодня я хочу продолжить рассказ о загрузке данных в таблицы SQL Server-а из разнообразных источников. На этот раз речь пойдет о данных, которые размещаются на Web-страницах в интернете. Предположим, есть ссылка, пройдя по которой можно просмотреть содержимое Web-страницы, включая набор таблиц с какими-либо данными. Мы напишем хранимую процедуру, которая получает в качестве параметра адрес сайта и возвращает несколько табличных наборов записей, каждый из которых соответствует таблице на сайте.
Для импорта таблиц со страницы сайта процедура будет получать html-код страницы, который затем будет интерпретироваться. Под таблицей будет пониматься html-таблица, то есть часть html-кода страницы, которая заключена в тэги <TABLE>.
Метод позволит, например, создавать информационные системы, которые смогут в режиме реального времени отслеживать различную информацию. Например, динамику курсов валют различных банков, информацию о праздниках в различных регионах, прогнозы погоды и многое другое.
Для начала напишем хранимую процедуру, которая получает в качестве параметра адрес сайта и возвращает html-код соответствующей Web-страницы. Сначала я хотел воспользоваться хранимыми процедурами sp_OACreate, sp_OAMethod, и т. д. То есть с помощью sp_OACreate создавать объект InternetExplorer.Application. А затем применять метод InnerHtml для получения html-кода страницы. Однако я "нарвался" на один сюрприз. Когда html-код занимает не больше 8 тыс. символов, то можно вызвать процедуру sp_OAGetProperty с выходным параметром, который и даст html-код. Если код занимает больше 8 тыс. символов, то методика не работает как описано в моей предыдущей статье об импорте таблиц из Word-файлов. Там же рассказывается о том, что для получения длинных строк можно вызвать sp_OAMethod/sp_OAGetProperty, указав null вместо выходного параметра, и поймать возвращаемое значение с помощью конструкции insert into ... exec sp_OAMethod/spOAGetProperty. И в данном случае это срабатывало, пока на моем новом компьютере стояла старая версия Internet Explorer-а (кажется 6-ая). После установки последней версии браузера, метод с вызовом кода insert into ... exec sp_OAGetProperty ... перестал работать. Процедура sp_OAGetErrorInfo показала, что при вызове sp_OAGetProperty с null вместо выходного параметра происходит ошибка с описанием: Traversal string: An object return value was required but not supplied. Источник ошибки: ODSOLE Extended Procedure. Поэтому вместо использования устаревшей технологии в данном случае необходимо воспользоваться clr-сборкой. Мы создадим скалярную функцию на базе clr-сборки, которая решает задачу загрузки html-кода Web-страницы. Код сборки такой:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
using System.Net;
public partial class HtmlCodeOperations
{
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlString GetHtmlCodeOfWebPage(SqlString url)
{
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(url.ToString());
myRequest.Method = "GET";
WebResponse myResponse = myRequest.GetResponse();
StreamReader srUTF8 = new StreamReader(myResponse.GetResponseStream(), System.Text.Encoding.UTF8);
string resultUTF8 = srUTF8.ReadToEnd();
srUTF8.Close();
myResponse.Close();
string result = "";
int[] utf8 = new int[4];
utf8[0] = resultUTF8.IndexOf(@"charset=utf-8");
utf8[1] = resultUTF8.IndexOf(@"charset=UTF-8");
utf8[2] = resultUTF8.IndexOf(@"charset=""utf-8""");
utf8[3] = resultUTF8.IndexOf(@"charset=""UTF-8""");
int[] win = new int[3];
win[0] = resultUTF8.IndexOf(@"windows-1251");
win[1] = resultUTF8.IndexOf(@"Windows-1251");
win[2] = resultUTF8.IndexOf(@"WINDOWS-1251");
int utfMin = -1, winMin = -1, i = 0;
for (i = 0; i < 4; i++)
{
if (i == 0)
{
utfMin = utf8[i];
}
else
{
if (utfMin < 0 && utf8[i] >= 0)
{
utfMin = utf8[i];
}
if (utfMin > utf8[i] && utf8[i] >= 0)
{
utfMin = utf8[i];
}
}
}
for (i = 0; i < 3; i++)
{
if (i == 0)
{
winMin = win[i];
}
else
{
if (winMin < 0 && win[i] >= 0)
{
winMin = win[i];
}
if (winMin > win[i] && win[i] >= 0)
{
winMin = win[i];
}
}
}
if ((utfMin < winMin && utfMin >= 0) || winMin < 0)
{
result = resultUTF8;
result = win[0].ToString() + " test " + utfMin.ToString() + " " + winMin.ToString() + " " + resultUTF8;
}
else
{
myRequest = (HttpWebRequest)WebRequest.Create(url.ToString());
myRequest.Method = "GET";
myResponse = myRequest.GetResponse();
StreamReader srWin1251 = new StreamReader(myResponse.GetResponseStream(),
System.Text.Encoding.GetEncoding("Windows-1251"));
string resultWin1251 = srWin1251.ReadToEnd();
srWin1251.Close();
result = utfMin.ToString() + " " + winMin.ToString() + " " + resultWin1251;
}
return new SqlString(result);
}
}
В коде выше html-код считывается с указанием определенной кодировки. Коды html, которые я встречал, используют либо кодировку UTF8, либо Windows-1251. Только на них мы и заложимся. В коде сборки используется метод IndexOf для того, чтобы найти первое вхождение подстроки "charset=UTF-8". При этом учитываются регистры символов. Также находится первое вхождение подстроки "windows-1251". Если подстрока с "UTF8" встречается раньше чем с "windows-1251", или "windows-1251" отсутствует, то возвращается код страницы в кодировке UTF8. В противном случае код страницы считывается повторно с указанием кодировки windows-1251. Если прочесть код в неверной кодировке, то не удастся получить в нормальном виде значения элементов, которые написаны с использованием кирилицы. Если вы найдете html-коды, в которых используется какая-то иная кодировка, то можно соответствующим образом подправить код сборки.
Теперь необходимо развернуть сборку на сервере. Поскольку сборка обращается к данным Internet, она должна быть помечена как unsafe. В то же время я не сторонник включения свойства trustworthy базы данных ради работы небезопасных сборок. Поэтому предварительно создадим асимметричный ключ. Введем в командной строке инструкцию, которая сгенерирует асимметричный ключ с помощью программы sn.exe:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin\sn.exe"-k"C:\Документы\Временные\tmp\HtmlWorkClr.snk"
Теперь подпишем сборку созданным ключом. Его нужно выбрать в свойствах проекта Visual Studio в разделе подписи:
Внедрим ключ на сервер:
use master
create asymmetric key HtmlWork
from file = N'C:\Документы\Временные\tmp\HtmlWorkClr.snk'
encryption by password = N'привет всем!!!'
Создаем логин на основе созданного ключа:
create login HtmlWorkLog from asymmetric key HtmlWork
Предоставляем логину доступ на запуск небезопасной сборки:
grant unsafe assembly to HtmlWorkLog
Теперь, наконец создадим сборку:
create assembly HtmlWrk
from N'C:\Документы\Временные\tmp\Database1\Database1\bin\Debug\Database1.dll'
with permission_set=unsafe
Такой способ развертывания сборки более безопасный. Можно было бы действовать с другой стороны. Создать в базе данных master сертификат и логин на его основе. Предоставить логину доступ на небезопасные сборки. Затем создать резервную копию сертификата, восстановить его в базе данных, создать на основе восстановленного сертификата пользователя. После чего подписать сборку созданным сертификатом, используя инструкцию add signature.
Код ниже создает функцию на основе созданной сборки, которая возвращает html-код созданной страницы:
create function dbo.HtmlCodeGet ( @url nvarchar ( 200 ) )
returns nvarchar ( max )
as external name HtmlWrk.HtmlCodeOperations.GetHtmlCodeOfWebPage
После того как решена задача получения кода html-страницы, необходимо научиться интерпретировать ее содержимое. Основная идея состоит в том, что табличные данные сайтов зачастую сосредоточены внутри html-таблиц, то есть, заключены внутри открывающего и закрывающего тэгов TABLE. Только такие данные и будут интерпретированы в нашей задаче. Такие html-таблицы напоминают xml-документы, в которых тэг TABLE является корневым.
Предположим, что код html-таблицы уже отредактирован, так чтобы соответствовать синтаксису xml. Тогда для извлечения данных мы воспользуемся функцией openxml и тем фактом, что тэги TR и TD используются для создания строк и столбцов таблицы. Это дает возможность построить выражение для поиска значений ячеек. Для изучения структуры xml-документа используется функция openxml. Функция openxml вызывается без параметров. Связано это с тем, что в одной ячейке таблицы может находиться не просто одно значение. Там может быть, например, целый список или внутри строки значения могут располагаться дополнительные тэги, которые обеспечивают нужное форматирование документа (размер шрифтов и прочее). Вывод функции openxml используется для нахождения числа строк и столбцов. Данные о числе столбцов используются для построения динамического sql-выражения, которое строит временную таблицу, которая будет содержать данные. Как обычно в таких случаях временная таблица является глобальной в связи с использованием динамического sql.
Когда таблица построена, начинается цикл по строкам html-таблицы. На каждой его итерации идет цикл по столбцам. На каждой итерации внутреннего цикла во временную таблицу кладется значение очередной ячейки. Оно получается применением метода value типа данных xml.
В данном случае мы не анализируем содержимое ячейки на предмет наличия в ней иных тэгов. Поэтому если, скажем, в ячейке есть список, то его содержимое будет конкатенироваться и в таком виде загружаться в таблицу. Можно было бы считывать значение ячейки с помощью метода query типа данных xml, получая содержимое ячейки в формате xml. К нему можно было бы снова применять функцию openxml без параметров для определения тэгов, находящихся внутри ячейки и определения значений элементов, которые образуют отдельную ячейку. Но мы оставляем это в качестве упражнения читателю.
Описанный алгоритм реализован в нижеследующей хранимой процедуре.
if sessionproperty ( N'quoted_identifier' ) = 0
set quoted_identifier on
go
exec sp_xml_removedocument @h
set @curtd = 1
set @sql = N''
while @curtd <= @td
begin
set @sql = @sql + case when @curtd = 1 then N'' else N', ' end + N'row' + cast ( @curtd as nvarchar ( 100 ) ) + N' varchar ( 8000 ) null'
set @curtd += 1
end
set @sql = N'iRowId int identity ( 1, 1 ) not null primary key clustered, ' + @sql
set @sql = N'
if object_id ( N''tempdb..' + @tab + ''', N''U'' ) is not null
drop table ' + @tab + '
create table ' + @tab + '
(
' + @sql + '
) on [PRIMARY]
'
exec sp_executesql @sql
set @curtr = 1
while @curtr <= @tr
begin
set @curtd = 1
while @curtd <= @td
begin
if @curtd = 1
begin
set @sql = N'
insert into ' + @tab + ' ( row1 )
select
case when @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' ) is not null
then @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
else @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/th[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
if sessionproperty ( N'ansi_nulls' ) = 0
if sessionproperty ( N'ansi_nulls' ) = 0
set ansi_nulls on
go
if sessionproperty ( N'quoted_identifier' ) = 0
set quoted_identifier on
go
alter proc [dbo].[MakeXmlFromTab]
(
@txt nvarchar ( max )
)
as
begin
declare @CurAttrStart int, @CurAttrEnd int, @txtPrev nvarchar ( max ) = @txt, @xml xml, @quote char ( 1 ), @isAttrEx bit = 1, @err nvarchar ( max )
-- пока используется предположение, что символ за концом значения атрибута, это или пробел или ">"
if @CurAttrStart = 0
begin
set @isAttrEx = 0
end
set @CurAttrEnd = case when charindex ( N'>', @txt, @CurAttrStart + 1 ) < charindex ( N' ', @txt, @CurAttrStart + 1 )
then charindex ( N'>', @txt, @CurAttrStart + 1 ) else charindex ( N' ', @txt, @CurAttrStart + 1 ) end
if charindex ( N'>', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N' ', @txt, @CurAttrStart + 1 )
end
if charindex ( N' ', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N'>', @txt, @CurAttrStart + 1 )
end
-- добавляем кавычки к значению первого атрибута
if substring ( @txt, @CurAttrStart + 1, 1 ) not in ( N'"', N'''' ) and substring ( @txt, @CurAttrEnd + 1, 1 ) not in ( N'"', N'''' )
begin
set @txt = stuff ( @txt, @CurAttrStart + 1, 0, N'"' )
set @txt = stuff ( @txt, @CurAttrEnd + 1, 0, N'"' )
end
end
if substring ( @txt, @CurAttrStart + 1, 1 ) in ( N'"', N'''' )
begin
-- это требуется, чтобы не нарваться на знак =, который находится внутри значения атрибута
set @quote = substring ( @txt, @CurAttrStart + 1, 1 )
set @CurAttrStart = charindex ( @quote, @txt, @CurAttrStart + 2 )
end
if @CurAttrStart = 0 or @CurAttrEnd = 0
begin
set @isAttrEx = 0
end
while @CurAttrStart <> 0 and @isAttrEx = 1
begin
set @CurAttrStart = charindex ( N'=', @txt, @CurAttrStart + 1 )
if @CurAttrStart = 0
begin
break
end
if substring ( @txt, @CurAttrStart + 1, 1 ) in ( N'"', N'''' )
begin
-- это требуется, чтобы не нарваться на знак =, который находится внутри значения атрибута
set @quote = substring ( @txt, @CurAttrStart + 1, 1 )
set @CurAttrStart = charindex ( @quote, @txt, @CurAttrStart + 2 )
continue
end
set @CurAttrEnd = 0
if charindex ( N'>', @txt, @CurAttrStart + 1 ) < charindex ( N' ', @txt, @CurAttrStart + 1 ) and charindex ( N'>', @txt, @CurAttrStart + 1 ) <> 0
begin
set @CurAttrEnd = charindex ( N'>', @txt, @CurAttrStart + 1 )
end
if charindex ( N' ', @txt, @CurAttrStart + 1 ) < charindex ( N'>', @txt, @CurAttrStart + 1 ) and charindex ( N' ', @txt, @CurAttrStart + 1 ) <> 0
end
if charindex ( N' ', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N'>', @txt, @CurAttrStart + 1 )
end
if charindex ( N'>', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N' ', @txt, @CurAttrStart + 1 )
end
break
end
if substring ( @txt, @CurAttrStart + 1, 1 ) not in ( N'"', N'''' ) and substring ( @txt, @CurAttrEnd + 1, 1 ) not in ( N'"', N'''' )
begin
set @txt = stuff ( @txt, @CurAttrStart + 1, 0, N'"' )
set @txt = stuff ( @txt, @CurAttrEnd + 1, 0, N'"' )
end
end
set @txt = replace ( @txt, N' ', N' ' )
set @txt = replace ( @txt, N' ', N' ' )
set @txt = replace ( @txt, N'&lang', N'' )
set @txt = replace ( @txt, N'&', N'' )
set @txt = replace ( @txt, N':', N'-' )
begin try
exec dbo.CheckDocTags @txt out
end try
begin catch
select @txt, error_message () [Описание ошибки], error_line () [Номер строки], error_procedure () [Процедура]
end catch
exec dbo.EliminateWrongAttr @txt out
set @xml = try_convert ( xml, @txt )
if @xml is null
begin
begin try
set @xml = cast ( @txt as xml )
end try
begin catch
set @err = error_message ()
end catch
select @txtPrev [Исходный текст], @txt [Преобразованный текст] , @err [Текст ошибки]
end
else
begin
begin try
exec dbo.ConvertXmlToTalbe @xml
end try
begin catch
select error_procedure () [Процедура], error_message () [Описание ошибки], error_line () [Номер строки], @xml [Документ]
end catch
end
end
До сих пор мы работали с html-таблицей. Но ее надо извлечь из html-документа. Для поиска таблицы мы, конечно, будем использовать имя тэга, то есть искать подстроки вида <TABLE. Но необходимо учесть, что внутри тэга TABLE может находиться другой такой тэг. Это вложенность может иметь нескольких уровней. Мы будем искать самый глубокий тэг TABLE и извлекать содержимое таблицы. Хранимая процедура dbo.FindTables решает задачу. Она в цикле извлекает все таблицы из документа и вызывает для таблицы хранимую процедуру dbo.MakeXmlFromTable.
)
as
begin
declare @StartTab int, @EndTab int, @StartNextTab int, @TabTxt nvarchar ( max ), @cnt int
while @StartTab <> 0
begin
set @StartTab = charindex ( N'<table', @txt, case when @cnt = 1 then 0 else 1 end + @StartTab )
break
end
if @StartNextTab < @EndTab
begin
while @StartNextTab <> 0 and @StartNextTab < @EndTab
begin
set @StartTab = charindex ( N'<table', @txt, @StartTab + 1 )
set @StartNextTab = charindex ( N'<table', @txt, @StartTab + 1 )
end
end
end
exec dbo.MakeXmlFromTab @TabTxt
set @cnt += 1
end
end
Вроде бы на этом все. Теперь можно протестировать работу группы процедур на примере сайта банка Москвы (http://www.bm.ru). Запустим такой код
В результате получаем информацию о таблицах сайта в привычном для SQL Server-а виде:
Для импорта таблиц со страницы сайта процедура будет получать html-код страницы, который затем будет интерпретироваться. Под таблицей будет пониматься html-таблица, то есть часть html-кода страницы, которая заключена в тэги <TABLE>.
Метод позволит, например, создавать информационные системы, которые смогут в режиме реального времени отслеживать различную информацию. Например, динамику курсов валют различных банков, информацию о праздниках в различных регионах, прогнозы погоды и многое другое.
Для начала напишем хранимую процедуру, которая получает в качестве параметра адрес сайта и возвращает html-код соответствующей Web-страницы. Сначала я хотел воспользоваться хранимыми процедурами sp_OACreate, sp_OAMethod, и т. д. То есть с помощью sp_OACreate создавать объект InternetExplorer.Application. А затем применять метод InnerHtml для получения html-кода страницы. Однако я "нарвался" на один сюрприз. Когда html-код занимает не больше 8 тыс. символов, то можно вызвать процедуру sp_OAGetProperty с выходным параметром, который и даст html-код. Если код занимает больше 8 тыс. символов, то методика не работает как описано в моей предыдущей статье об импорте таблиц из Word-файлов. Там же рассказывается о том, что для получения длинных строк можно вызвать sp_OAMethod/sp_OAGetProperty, указав null вместо выходного параметра, и поймать возвращаемое значение с помощью конструкции insert into ... exec sp_OAMethod/spOAGetProperty. И в данном случае это срабатывало, пока на моем новом компьютере стояла старая версия Internet Explorer-а (кажется 6-ая). После установки последней версии браузера, метод с вызовом кода insert into ... exec sp_OAGetProperty ... перестал работать. Процедура sp_OAGetErrorInfo показала, что при вызове sp_OAGetProperty с null вместо выходного параметра происходит ошибка с описанием: Traversal string: An object return value was required but not supplied. Источник ошибки: ODSOLE Extended Procedure. Поэтому вместо использования устаревшей технологии в данном случае необходимо воспользоваться clr-сборкой. Мы создадим скалярную функцию на базе clr-сборки, которая решает задачу загрузки html-кода Web-страницы. Код сборки такой:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.IO;
using System.Net;
public partial class HtmlCodeOperations
{
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlString GetHtmlCodeOfWebPage(SqlString url)
{
HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(url.ToString());
myRequest.Method = "GET";
WebResponse myResponse = myRequest.GetResponse();
StreamReader srUTF8 = new StreamReader(myResponse.GetResponseStream(), System.Text.Encoding.UTF8);
string resultUTF8 = srUTF8.ReadToEnd();
srUTF8.Close();
myResponse.Close();
string result = "";
int[] utf8 = new int[4];
utf8[0] = resultUTF8.IndexOf(@"charset=utf-8");
utf8[1] = resultUTF8.IndexOf(@"charset=UTF-8");
utf8[2] = resultUTF8.IndexOf(@"charset=""utf-8""");
utf8[3] = resultUTF8.IndexOf(@"charset=""UTF-8""");
int[] win = new int[3];
win[0] = resultUTF8.IndexOf(@"windows-1251");
win[1] = resultUTF8.IndexOf(@"Windows-1251");
win[2] = resultUTF8.IndexOf(@"WINDOWS-1251");
int utfMin = -1, winMin = -1, i = 0;
for (i = 0; i < 4; i++)
{
if (i == 0)
{
utfMin = utf8[i];
}
else
{
if (utfMin < 0 && utf8[i] >= 0)
{
utfMin = utf8[i];
}
if (utfMin > utf8[i] && utf8[i] >= 0)
{
utfMin = utf8[i];
}
}
}
for (i = 0; i < 3; i++)
{
if (i == 0)
{
winMin = win[i];
}
else
{
if (winMin < 0 && win[i] >= 0)
{
winMin = win[i];
}
if (winMin > win[i] && win[i] >= 0)
{
winMin = win[i];
}
}
}
if ((utfMin < winMin && utfMin >= 0) || winMin < 0)
{
result = resultUTF8;
result = win[0].ToString() + " test " + utfMin.ToString() + " " + winMin.ToString() + " " + resultUTF8;
}
else
{
myRequest = (HttpWebRequest)WebRequest.Create(url.ToString());
myRequest.Method = "GET";
myResponse = myRequest.GetResponse();
StreamReader srWin1251 = new StreamReader(myResponse.GetResponseStream(),
System.Text.Encoding.GetEncoding("Windows-1251"));
string resultWin1251 = srWin1251.ReadToEnd();
srWin1251.Close();
result = utfMin.ToString() + " " + winMin.ToString() + " " + resultWin1251;
}
return new SqlString(result);
}
}
В коде выше html-код считывается с указанием определенной кодировки. Коды html, которые я встречал, используют либо кодировку UTF8, либо Windows-1251. Только на них мы и заложимся. В коде сборки используется метод IndexOf для того, чтобы найти первое вхождение подстроки "charset=UTF-8". При этом учитываются регистры символов. Также находится первое вхождение подстроки "windows-1251". Если подстрока с "UTF8" встречается раньше чем с "windows-1251", или "windows-1251" отсутствует, то возвращается код страницы в кодировке UTF8. В противном случае код страницы считывается повторно с указанием кодировки windows-1251. Если прочесть код в неверной кодировке, то не удастся получить в нормальном виде значения элементов, которые написаны с использованием кирилицы. Если вы найдете html-коды, в которых используется какая-то иная кодировка, то можно соответствующим образом подправить код сборки.
Теперь необходимо развернуть сборку на сервере. Поскольку сборка обращается к данным Internet, она должна быть помечена как unsafe. В то же время я не сторонник включения свойства trustworthy базы данных ради работы небезопасных сборок. Поэтому предварительно создадим асимметричный ключ. Введем в командной строке инструкцию, которая сгенерирует асимметричный ключ с помощью программы sn.exe:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin\sn.exe"-k"C:\Документы\Временные\tmp\HtmlWorkClr.snk"
Теперь подпишем сборку созданным ключом. Его нужно выбрать в свойствах проекта Visual Studio в разделе подписи:
Внедрим ключ на сервер:
use master
create asymmetric key HtmlWork
from file = N'C:\Документы\Временные\tmp\HtmlWorkClr.snk'
encryption by password = N'привет всем!!!'
Создаем логин на основе созданного ключа:
create login HtmlWorkLog from asymmetric key HtmlWork
Предоставляем логину доступ на запуск небезопасной сборки:
grant unsafe assembly to HtmlWorkLog
Теперь, наконец создадим сборку:
create assembly HtmlWrk
from N'C:\Документы\Временные\tmp\Database1\Database1\bin\Debug\Database1.dll'
with permission_set=unsafe
Такой способ развертывания сборки более безопасный. Можно было бы действовать с другой стороны. Создать в базе данных master сертификат и логин на его основе. Предоставить логину доступ на небезопасные сборки. Затем создать резервную копию сертификата, восстановить его в базе данных, создать на основе восстановленного сертификата пользователя. После чего подписать сборку созданным сертификатом, используя инструкцию add signature.
Код ниже создает функцию на основе созданной сборки, которая возвращает html-код созданной страницы:
create function dbo.HtmlCodeGet ( @url nvarchar ( 200 ) )
returns nvarchar ( max )
as external name HtmlWrk.HtmlCodeOperations.GetHtmlCodeOfWebPage
После того как решена задача получения кода html-страницы, необходимо научиться интерпретировать ее содержимое. Основная идея состоит в том, что табличные данные сайтов зачастую сосредоточены внутри html-таблиц, то есть, заключены внутри открывающего и закрывающего тэгов TABLE. Только такие данные и будут интерпретированы в нашей задаче. Такие html-таблицы напоминают xml-документы, в которых тэг TABLE является корневым.
Предположим, что код html-таблицы уже отредактирован, так чтобы соответствовать синтаксису xml. Тогда для извлечения данных мы воспользуемся функцией openxml и тем фактом, что тэги TR и TD используются для создания строк и столбцов таблицы. Это дает возможность построить выражение для поиска значений ячеек. Для изучения структуры xml-документа используется функция openxml. Функция openxml вызывается без параметров. Связано это с тем, что в одной ячейке таблицы может находиться не просто одно значение. Там может быть, например, целый список или внутри строки значения могут располагаться дополнительные тэги, которые обеспечивают нужное форматирование документа (размер шрифтов и прочее). Вывод функции openxml используется для нахождения числа строк и столбцов. Данные о числе столбцов используются для построения динамического sql-выражения, которое строит временную таблицу, которая будет содержать данные. Как обычно в таких случаях временная таблица является глобальной в связи с использованием динамического sql.
Когда таблица построена, начинается цикл по строкам html-таблицы. На каждой его итерации идет цикл по столбцам. На каждой итерации внутреннего цикла во временную таблицу кладется значение очередной ячейки. Оно получается применением метода value типа данных xml.
В данном случае мы не анализируем содержимое ячейки на предмет наличия в ней иных тэгов. Поэтому если, скажем, в ячейке есть список, то его содержимое будет конкатенироваться и в таком виде загружаться в таблицу. Можно было бы считывать значение ячейки с помощью метода query типа данных xml, получая содержимое ячейки в формате xml. К нему можно было бы снова применять функцию openxml без параметров для определения тэгов, находящихся внутри ячейки и определения значений элементов, которые образуют отдельную ячейку. Но мы оставляем это в качестве упражнения читателю.
Описанный алгоритм реализован в нижеследующей хранимой процедуре.
if sessionproperty ( N'ansi_nulls' ) = 0
set ansi_nulls on
go
set ansi_nulls on
go
if sessionproperty ( N'quoted_identifier' ) = 0
set quoted_identifier on
go
if object_id ( N'dbo.ConvertXmlToTable', N'P' ) is null
exec ( N'create proc dbo.ConvertXmlToTable as return 1' )
go
exec ( N'create proc dbo.ConvertXmlToTable as return 1' )
go
alter
proc dbo.ConvertXmlToTalbe
(
@xml xml
)
as
begin
declare @td int, @tr int, @h int, @sql nvarchar ( max ), @curtd int, @tab nvarchar ( 100 ) = quotename ( N'##' + cast ( newid () as nvarchar ( 1000 ) ) )
declare @curtr int, @expr nvarchar ( max )
exec sp_xml_preparedocument @h out, @xml
(
@xml xml
)
as
begin
declare @td int, @tr int, @h int, @sql nvarchar ( max ), @curtd int, @tab nvarchar ( 100 ) = quotename ( N'##' + cast ( newid () as nvarchar ( 1000 ) ) )
declare @curtr int, @expr nvarchar ( max )
exec sp_xml_preparedocument @h out, @xml
if @xml.exist ( '/table/tbody' ) = 1 and @xml.exist ( '/table/tbody/tr' ) = 1
begin
set @expr = N'/table/tbody'
select @tr = count (*)
from openxml ( @h, N'/table/tbody', 1 )
where localname = N'tr'
if @tr = 0
from openxml ( @h, N'/table/tbody', 1 )
where localname in ( N'td', N'th' )
end
begin
set @expr = N'/table/tbody'
select @tr = count (*)
from openxml ( @h, N'/table/tbody', 1 )
where localname = N'tr'
if @tr = 0
begin
select 'Данные не опознаны.'
[Сообщение], @xml [Документ]
return
end
select @td = count (*) / @trfrom openxml ( @h, N'/table/tbody', 1 )
where localname in ( N'td', N'th' )
end
else
begin
set @expr = N'/table'
select @tr = count (*)
from openxml ( @h, N'/table', 1 )
where localname = N'tr'
if @tr = 0
select
@td = count (*) / @tr
from openxml ( @h, N'/table', 1 )
where localname in ( N'td', N'th' )
end
begin
set @expr = N'/table'
select @tr = count (*)
from openxml ( @h, N'/table', 1 )
where localname = N'tr'
if @tr = 0
begin
select 'Данные не опознаны.' [Сообщение], @xml [Документ]
return
end
from openxml ( @h, N'/table', 1 )
where localname in ( N'td', N'th' )
end
exec sp_xml_removedocument @h
set @curtd = 1
set @sql = N''
while @curtd <= @td
begin
set @sql = @sql + case when @curtd = 1 then N'' else N', ' end + N'row' + cast ( @curtd as nvarchar ( 100 ) ) + N' varchar ( 8000 ) null'
set @curtd += 1
end
set @sql = N'iRowId int identity ( 1, 1 ) not null primary key clustered, ' + @sql
set @sql = N'
if object_id ( N''tempdb..' + @tab + ''', N''U'' ) is not null
drop table ' + @tab + '
create table ' + @tab + '
(
' + @sql + '
) on [PRIMARY]
'
exec sp_executesql @sql
set @curtr = 1
while @curtr <= @tr
begin
set @curtd = 1
while @curtd <= @td
begin
if @curtd = 1
begin
set @sql = N'
insert into ' + @tab + ' ( row1 )
select
case when @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' ) is not null
then @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
else @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/th[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
end
'
end
else
begin
set @sql = N'
update ' + @tab + '
set row' + cast ( @curtd as nvarchar ( 100 ) ) + ' =
case when @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' ) is not null
then @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
else @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/th[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
end
where iRowId = ' + cast ( @curtr as nvarchar ( 100 ) ) + '
'
end
exec sp_executesql @sql, N'@xml xml', @xml = @xml
set @curtd += 1
end
set @curtr += 1
end
set @sql = N'
select *
from ' + @tab + '
if object_id ( N''tempdb..' + @tab + ''', N''U'' ) is not null
drop table ' + @tab + '
'
exec sp_executesql @sql
end
В процедуре методом exists проверяется наличие элемента TBODY, внутри которого могут быть элементы TR и TD.
Теперь поговорим о том, как добиться того, чтобы html-таблица стала правильно оформленным xml-документом. К этому есть несколько препятствий. Одно из них состоит в том, что в html-документе может присутствовать тэг, для которого нет закрывающего тэга. Для решения этой проблемы мы напишем отдельную хранимую процедуру, которая будет искать все места, с которых начинаются открывающие и закрывающие тэги.
Если есть тэг для которого нет закрывающего тэга, то выбор места где требуется расположить закрывающий тэг необднозначен, и мы будет просто добавлять к такому тэгу, то есть жедать его тэгом-контейнером. Если же будет найден закрывающий тэг, для которого нет открывающего тэга, то такой тэг будет просто удаляться. При этом, если будут найдены открывающий и закрывающий тэги одного элемента, между которыми нет тэга с таким же элементом, то мы будет предполагать, что эти тэги относятся друг к другу и с ними ничего происходить не будет. Также не будут учитываться комментарии, которые будут находиться по подстроке "<!".
В процедуре будут использоваться две временные таблицы, в которых будут храниться номера символов, с которых начинаются закрывающие и открывающие тэги. Для построения таблицы, в которой будут столбцы с началом открывающего и закрывающего тэгов, будет использоваться запрос к двум таблицам с функцией lead, которая появилась в MS SQL 2012. Она позволяет писать более эффективные запросы и уменьшить объем кода. Ниже код хранимой процедуры CheckDocTags.
'
end
else
begin
set @sql = N'
update ' + @tab + '
set row' + cast ( @curtd as nvarchar ( 100 ) ) + ' =
case when @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' ) is not null
then @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/td[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
else @xml.value ( N''(' + @expr + '/tr[' + cast ( @curtr as nvarchar ( 100 ) ) + ']/th[' + cast ( @curtd as nvarchar ( 100 ) ) + '])[1]'', N''nvarchar ( max )'' )
end
where iRowId = ' + cast ( @curtr as nvarchar ( 100 ) ) + '
'
end
exec sp_executesql @sql, N'@xml xml', @xml = @xml
set @curtd += 1
end
set @curtr += 1
end
set @sql = N'
select *
from ' + @tab + '
if object_id ( N''tempdb..' + @tab + ''', N''U'' ) is not null
drop table ' + @tab + '
'
exec sp_executesql @sql
end
В процедуре методом exists проверяется наличие элемента TBODY, внутри которого могут быть элементы TR и TD.
Теперь поговорим о том, как добиться того, чтобы html-таблица стала правильно оформленным xml-документом. К этому есть несколько препятствий. Одно из них состоит в том, что в html-документе может присутствовать тэг, для которого нет закрывающего тэга. Для решения этой проблемы мы напишем отдельную хранимую процедуру, которая будет искать все места, с которых начинаются открывающие и закрывающие тэги.
Если есть тэг для которого нет закрывающего тэга, то выбор места где требуется расположить закрывающий тэг необднозначен, и мы будет просто добавлять к такому тэгу, то есть жедать его тэгом-контейнером. Если же будет найден закрывающий тэг, для которого нет открывающего тэга, то такой тэг будет просто удаляться. При этом, если будут найдены открывающий и закрывающий тэги одного элемента, между которыми нет тэга с таким же элементом, то мы будет предполагать, что эти тэги относятся друг к другу и с ними ничего происходить не будет. Также не будут учитываться комментарии, которые будут находиться по подстроке "<!".
В процедуре будут использоваться две временные таблицы, в которых будут храниться номера символов, с которых начинаются закрывающие и открывающие тэги. Для построения таблицы, в которой будут столбцы с началом открывающего и закрывающего тэгов, будет использоваться запрос к двум таблицам с функцией lead, которая появилась в MS SQL 2012. Она позволяет писать более эффективные запросы и уменьшить объем кода. Ниже код хранимой процедуры CheckDocTags.
if sessionproperty ( N'ansi_nulls' ) = 0
set ansi_nulls on
go
go
go
@txt nvarchar ( max ) out
)
as
begin
set nocount, xact_abort on
create table #OpenTags
(
iRowId int identity ( 1, 1 ) not null,
TagName nvarchar ( 1000 ) not null,
StartPos int not null,
IsClosed bit not null,
primary key clustered ( StartPos asc, TagName asc ) on [PRIMARY]
) on [PRIMARY]
create table #CloseTags
(
iRowId int identity ( 1, 1 ) not null,
TagName nvarchar ( 1000 ) not null,
StartPos int not null,
primary key clustered ( StartPos asc, TagName asc ) on [PRIMARY]
) on [PRIMARY]
while @pos <> 0
begin
set @pos = charindex ( N'<', @txt, @pos + case when @iter = 0 then 0 else 1 end )
if @pos = 0
begin
break
end
if substring ( @txt, @pos + 1, 1 ) = N'/'
begin
continue
end
continue
end
set @tag = substring ( @txt, @pos + 1, charindex ( N'>', @txt, @pos + 1 ) - @pos - 1 )
end
else if charindex ( N'>', @txt, @pos + 1 ) = 0
begin
set @tag = substring ( @txt, @pos + 1, charindex ( N' ', @txt, @pos + 1 ) - @pos - 1 )
end
else if charindex ( N' ', @txt, @pos + 1 ) < charindex ( N'>', @txt, @pos + 1 )
begin
set @tag = substring ( @txt, @pos + 1, charindex ( N' ', @txt, @pos + 1 ) - @pos - 1 )
end
else
begin
set @tag = substring ( @txt, @pos + 1, charindex ( N'>', @txt, @pos + 1 ) - @pos - 1 )
end
begin
set @IsClosed = 1
end
else
begin
set @IsClosed = 0
end
set @iter += 1
end
while @pos <> 0
begin
set @pos = charindex ( N'</', @txt, @pos + case when @iter = 0 then 0 else 1 end )
if @pos = 0
begin
break
end
values ( @tag, @pos )
set @iter += 1
end
if object_id ( N'tempdb..#Tags', N'U' ) is not null
drop table #Tags
create table #Tags
(
TagName nvarchar ( max ) not null,
StartPos int not null,
Direction bit not null,
iRowId int not null
) on [PRIMARY]
begin
delete #Tags
from #OpenTags
union all
select TagName, StartPos, 1, iRowId
from #CloseTags
as
(
select TagName, StartPos, Direction, iRowId,
lead ( Direction ) over ( partition by TagName order by TagName, StartPos ) Direction_next,
lead ( iRowId ) over ( partition by TagName order by TagName, StartPos ) iRowId_next
from #Tags
)
delete openTags
from
Data
inner join
#OpenTags openTags on openTags.iRowId = Data.iRowId
where Data.Direction = 0 and Data.Direction_next = 1
;
with Data
as
(
select TagName, StartPos, Direction, iRowId,
lead ( Direction ) over ( partition by TagName order by TagName, StartPos ) Direction_next,
lead ( iRowId ) over ( partition by TagName order by TagName, StartPos ) iRowId_next
from #Tags
)
delete closeTags
from
Data
inner join
#CloseTags closeTags on closeTags.iRowId = Data.iRowId_next
where Data.Direction = 0 and Data.Direction_next = 1
set @kol = @@rowcount
end
declare cur cursor local static forward_only for
select TagName, StartPos
from #CloseTags
order by StartPos
open cur
fetch next from cur into @Tag, @pos
while @@fetch_status = 0
begin
set @pos -= @kol
set @EndTag = charindex ( N'>', @txt, @pos )
set @txt = stuff ( @txt, @pos, @EndTag - @pos + 1, N'' )
update #OpenTags
set StartPos = StartPos - ( @EndTag - @pos + 1 )
where StartPos >= @pos
end
close cur
deallocate cur
select TagName, StartPos
from #OpenTags
order by StartPos
open cur
fetch next from cur into @Tag, @pos
while @@fetch_status = 0
begin
set @pos += 2 * @step
set @EndTag = charindex ( N'>', @txt, @pos )
set @txt = stuff ( @txt, @EndTag, 0, ' /' )
set @step += 1
fetch next from cur into @Tag, @pos
end
close cur
deallocate cur
end
go
if
sessionproperty ( N'quoted_identifier'
) = 0
set quoted_identifier ongo
if
object_id ( N'dbo.CheckDocTags', N'P' ) is null
exec ( N'create proc
dbo.CheckDocTags as return 1' )go
alter
proc dbo.CheckDocTags
(@txt nvarchar ( max ) out
)
as
begin
set nocount, xact_abort on
if object_id ( N'tempdb..#OpenTags', N'U' ) is not null
drop
table #OpenTagscreate table #OpenTags
(
iRowId int identity ( 1, 1 ) not null,
TagName nvarchar ( 1000 ) not null,
StartPos int not null,
IsClosed bit not null,
primary key clustered ( StartPos asc, TagName asc ) on [PRIMARY]
) on [PRIMARY]
if object_id ( N'tempdb..#CloseTags', N'U' ) is not null
drop
table #CloseTagscreate table #CloseTags
(
iRowId int identity ( 1, 1 ) not null,
TagName nvarchar ( 1000 ) not null,
StartPos int not null,
primary key clustered ( StartPos asc, TagName asc ) on [PRIMARY]
) on [PRIMARY]
declare @pos int, @iter int, @tag nvarchar ( max ), @IsClosed bit, @EndTag int
set @iter = 0
set @pos = 1while @pos <> 0
begin
set @pos = charindex ( N'<', @txt, @pos + case when @iter = 0 then 0 else 1 end )
if @pos = 0
begin
break
end
if substring ( @txt, @pos + 1, 1 ) = N'/'
begin
continue
end
if substring ( @txt, @pos + 1, 1 ) = N'!'
begincontinue
end
if charindex ( N' ', @txt, @pos + 1 ) = 0
beginset @tag = substring ( @txt, @pos + 1, charindex ( N'>', @txt, @pos + 1 ) - @pos - 1 )
end
else if charindex ( N'>', @txt, @pos + 1 ) = 0
begin
set @tag = substring ( @txt, @pos + 1, charindex ( N' ', @txt, @pos + 1 ) - @pos - 1 )
end
else if charindex ( N' ', @txt, @pos + 1 ) < charindex ( N'>', @txt, @pos + 1 )
begin
set @tag = substring ( @txt, @pos + 1, charindex ( N' ', @txt, @pos + 1 ) - @pos - 1 )
end
else
begin
set @tag = substring ( @txt, @pos + 1, charindex ( N'>', @txt, @pos + 1 ) - @pos - 1 )
end
set
@EndTag = charindex ( N'>', @txt, @pos + 1 )
if substring ( @txt, @EndTag - 1, 1 ) = N'/'begin
set @IsClosed = 1
end
else
begin
set @IsClosed = 0
end
insert
into #OpenTags ( TagName, StartPos, IsClosed )
values
( @tag, @pos, @IsClosed )set @iter += 1
end
set @pos = 1
set @iter = 0while @pos <> 0
begin
set @pos = charindex ( N'</', @txt, @pos + case when @iter = 0 then 0 else 1 end )
if @pos = 0
begin
break
end
set
@tag = substring ( @txt, @pos + 2, charindex ( N'>', @txt, @pos ) - @pos - 2 )
insert
into #CloseTags ( TagName, StartPos )values ( @tag, @pos )
set @iter += 1
end
delete #OpenTags
where
IsClosed = 1if object_id ( N'tempdb..#Tags', N'U' ) is not null
drop table #Tags
create table #Tags
(
TagName nvarchar ( max ) not null,
StartPos int not null,
Direction bit not null,
iRowId int not null
) on [PRIMARY]
declare @kol int = 1
while @kol <> 0begin
delete #Tags
insert
into #Tags ( TagName, StartPos, Direction, iRowId )
select
TagName, StartPos, 0 Direction, iRowIdfrom #OpenTags
union all
select TagName, StartPos, 1, iRowId
from #CloseTags
;
with Dataas
(
select TagName, StartPos, Direction, iRowId,
lead ( Direction ) over ( partition by TagName order by TagName, StartPos ) Direction_next,
lead ( iRowId ) over ( partition by TagName order by TagName, StartPos ) iRowId_next
from #Tags
)
delete openTags
from
Data
inner join
#OpenTags openTags on openTags.iRowId = Data.iRowId
where Data.Direction = 0 and Data.Direction_next = 1
;
with Data
as
(
select TagName, StartPos, Direction, iRowId,
lead ( Direction ) over ( partition by TagName order by TagName, StartPos ) Direction_next,
lead ( iRowId ) over ( partition by TagName order by TagName, StartPos ) iRowId_next
from #Tags
)
delete closeTags
from
Data
inner join
#CloseTags closeTags on closeTags.iRowId = Data.iRowId_next
where Data.Direction = 0 and Data.Direction_next = 1
set @kol = @@rowcount
end
declare @step int
set @kol = 0declare cur cursor local static forward_only for
select TagName, StartPos
from #CloseTags
order by StartPos
open cur
fetch next from cur into @Tag, @pos
while @@fetch_status = 0
begin
set @pos -= @kol
set @EndTag = charindex ( N'>', @txt, @pos )
set @txt = stuff ( @txt, @pos, @EndTag - @pos + 1, N'' )
update #OpenTags
set StartPos = StartPos - ( @EndTag - @pos + 1 )
where StartPos >= @pos
set
@kol += @EndTag - @pos + 1
fetch
next from cur into @Tag, @posend
close cur
deallocate cur
set @step = 0
declare cur cursor local static forward_only forselect TagName, StartPos
from #OpenTags
order by StartPos
open cur
fetch next from cur into @Tag, @pos
while @@fetch_status = 0
begin
set @pos += 2 * @step
set @EndTag = charindex ( N'>', @txt, @pos )
set @txt = stuff ( @txt, @EndTag, 0, ' /' )
set @step += 1
fetch next from cur into @Tag, @pos
end
close cur
deallocate cur
end
Следующее препятствие состоит в том, что после имени элемента может идти имя атрибута, за которым не стоит знак равенства. Такие псевдо-атрибуты будут удаляться процедурой EliminateWrongAttr.
go
go
if object_id ( N'dbo.EliminateWrongAttr', N'P' ) is null
exec ( N'create proc dbo. EliminateWrongAttr as return 1' )
go
alter proc dbo.EliminateWrongAttr
(
@txt nvarchar ( max ) out
)
as
begin
set nocount, xact_abort on
begin
set @pos = charindex ( '<', @txt, case when @cnt = 1 then @pos else @pos + 1 end )
set @cnt += 1
break
end
set @NewAttr = charindex ( N' ', @txt, @pos + 1 )
if @NewAttr > charindex ( N'>', @txt, @pos + 1 ) or @NewAttr = 0
begin
continue
end
if charindex ( N' ', @txt, @NewAttr + 1 ) = 0
begin
set @EndNewAttr = charindex ( N'>', @txt, @NewAttr + 1 )
end
if charindex ( N'>', @txt, @NewAttr + 1 ) = 0
begin
set @EndNewAttr = charindex ( N' ', @txt, @NewAttr + 1 )
end
if charindex ( N' ', @txt, @NewAttr + 1 ) <> 0 and
charindex ( N'>', @txt, @NewAttr + 1 ) <> 0
begin
set @EndNewAttr = case when charindex ( N' ', @txt, @NewAttr + 1 ) < charindex ( N'>', @txt, @NewAttr + 1 ) then charindex ( N' ', @txt, @NewAttr + 1 )
else charindex ( N'>', @txt, @NewAttr + 1 ) end
end
if @EndNewAttr = 0
begin
continue
end
if charindex ( N'>', @txt, @NewAttr ) <= @EndNewAttr
begin
continue
end
if charindex ( N'=', @txt, @NewAttr ) = 0 or charindex ( N'=', @txt, @NewAttr ) > @EndNewAttr
begin
set @txt = stuff ( @txt, @NewAttr, @EndNewAttr - @NewAttr, N'' )
end
end
end
Теперь необходимо учесть, чтобы значение каждого атрибута было заключено в двойные кавычки. Надо сказать, что некоторые атрибуты могут и так быть в кавычках, необходимо отслеживать значение атрибутов, не заключенные в кавычки и добавлять их.
Для поиска значений атрибутов будет использоваться то, что перед ними стоит знак равно. Здесь потребуется быть внимательным. Значение атрибута, которое уже заключено в кавычки, может содержать пробелы и другие символы равенства, поэтому в таких случаях всегда требуется искать место окончания значения атрибута по знаку закрывающей кавычки. И уже после этого продолжать поиск следующих атрибутов. Следует также учитывать, что атрибуты могут быть также заключены в одиночные кавычки. В этом случае на них также не требуется воздействовать. Дополнительно необходимо устранять специальные символы, начинающиеся на знак амперсанда (&). Также знаки двоеточия заменяются символом тире на случай, если будут испольщоваться необъявленные пространства имен. Процедура ниже решает задачу. В конце она вызывает процедуры dbo.CheckDocTags, dbo.ConvertXmlToTable и dbo.EliminateWrongAttr.
if
sessionproperty ( N'ansi_nulls' ) = 0
set ansi_nulls ongo
if
sessionproperty ( N'quoted_identifier'
) = 0
set quoted_identifier ongo
if object_id ( N'dbo.EliminateWrongAttr', N'P' ) is null
exec ( N'create proc dbo. EliminateWrongAttr as return 1' )
go
alter proc dbo.EliminateWrongAttr
(
@txt nvarchar ( max ) out
)
as
begin
set nocount, xact_abort on
declare @pos int, @cnt int = 1, @NewAttr int, @EndNewAttr int
set @pos = 1
while @pos <> 0begin
set @pos = charindex ( '<', @txt, case when @cnt = 1 then @pos else @pos + 1 end )
set @cnt += 1
if @pos = 0
beginbreak
end
set @NewAttr = charindex ( N' ', @txt, @pos + 1 )
if @NewAttr > charindex ( N'>', @txt, @pos + 1 ) or @NewAttr = 0
begin
continue
end
if charindex ( N' ', @txt, @NewAttr + 1 ) = 0
begin
set @EndNewAttr = charindex ( N'>', @txt, @NewAttr + 1 )
end
if charindex ( N'>', @txt, @NewAttr + 1 ) = 0
begin
set @EndNewAttr = charindex ( N' ', @txt, @NewAttr + 1 )
end
if charindex ( N' ', @txt, @NewAttr + 1 ) <> 0 and
charindex ( N'>', @txt, @NewAttr + 1 ) <> 0
begin
set @EndNewAttr = case when charindex ( N' ', @txt, @NewAttr + 1 ) < charindex ( N'>', @txt, @NewAttr + 1 ) then charindex ( N' ', @txt, @NewAttr + 1 )
else charindex ( N'>', @txt, @NewAttr + 1 ) end
end
if @EndNewAttr = 0
begin
continue
end
if charindex ( N'>', @txt, @NewAttr ) <= @EndNewAttr
begin
continue
end
if charindex ( N'=', @txt, @NewAttr ) = 0 or charindex ( N'=', @txt, @NewAttr ) > @EndNewAttr
begin
set @txt = stuff ( @txt, @NewAttr, @EndNewAttr - @NewAttr, N'' )
end
end
end
Теперь необходимо учесть, чтобы значение каждого атрибута было заключено в двойные кавычки. Надо сказать, что некоторые атрибуты могут и так быть в кавычках, необходимо отслеживать значение атрибутов, не заключенные в кавычки и добавлять их.
if sessionproperty ( N'ansi_nulls' ) = 0
set ansi_nulls on
go
set quoted_identifier on
go
if object_id ( N'dbo.MakeXmlFromTab, N'P' ) is null
exec ( N'create proc dbo.MakeXmlFromTab as return 1' )
go
exec ( N'create proc dbo.MakeXmlFromTab as return 1' )
go
alter proc [dbo].[MakeXmlFromTab]
(
@txt nvarchar ( max )
)
as
begin
declare @CurAttrStart int, @CurAttrEnd int, @txtPrev nvarchar ( max ) = @txt, @xml xml, @quote char ( 1 ), @isAttrEx bit = 1, @err nvarchar ( max )
-- пока используется предположение, что символ за концом значения атрибута, это или пробел или ">"
-- место начала и конца значения атрибута
set @CurAttrStart = charindex ( N'=', @txt, 1 )if @CurAttrStart = 0
begin
set @isAttrEx = 0
end
if @isAttrEx = 1
beginset @CurAttrEnd = case when charindex ( N'>', @txt, @CurAttrStart + 1 ) < charindex ( N' ', @txt, @CurAttrStart + 1 )
then charindex ( N'>', @txt, @CurAttrStart + 1 ) else charindex ( N' ', @txt, @CurAttrStart + 1 ) end
if charindex ( N'>', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N' ', @txt, @CurAttrStart + 1 )
end
if charindex ( N' ', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N'>', @txt, @CurAttrStart + 1 )
end
-- добавляем кавычки к значению первого атрибута
if substring ( @txt, @CurAttrStart + 1, 1 ) not in ( N'"', N'''' ) and substring ( @txt, @CurAttrEnd + 1, 1 ) not in ( N'"', N'''' )
begin
set @txt = stuff ( @txt, @CurAttrStart + 1, 0, N'"' )
set @txt = stuff ( @txt, @CurAttrEnd + 1, 0, N'"' )
end
end
if substring ( @txt, @CurAttrStart + 1, 1 ) in ( N'"', N'''' )
begin
-- это требуется, чтобы не нарваться на знак =, который находится внутри значения атрибута
set @quote = substring ( @txt, @CurAttrStart + 1, 1 )
set @CurAttrStart = charindex ( @quote, @txt, @CurAttrStart + 2 )
end
if @CurAttrStart = 0 or @CurAttrEnd = 0
begin
set @isAttrEx = 0
end
while @CurAttrStart <> 0 and @isAttrEx = 1
begin
set @CurAttrStart = charindex ( N'=', @txt, @CurAttrStart + 1 )
if @CurAttrStart = 0
begin
break
end
if substring ( @txt, @CurAttrStart + 1, 1 ) in ( N'"', N'''' )
begin
-- это требуется, чтобы не нарваться на знак =, который находится внутри значения атрибута
set @quote = substring ( @txt, @CurAttrStart + 1, 1 )
set @CurAttrStart = charindex ( @quote, @txt, @CurAttrStart + 2 )
continue
end
set @CurAttrEnd = 0
if charindex ( N'>', @txt, @CurAttrStart + 1 ) < charindex ( N' ', @txt, @CurAttrStart + 1 ) and charindex ( N'>', @txt, @CurAttrStart + 1 ) <> 0
begin
set @CurAttrEnd = charindex ( N'>', @txt, @CurAttrStart + 1 )
end
if charindex ( N' ', @txt, @CurAttrStart + 1 ) < charindex ( N'>', @txt, @CurAttrStart + 1 ) and charindex ( N' ', @txt, @CurAttrStart + 1 ) <> 0
begin
set
@CurAttrEnd = charindex ( N' ', @txt, @CurAttrStart + 1 )end
if charindex ( N' ', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N'>', @txt, @CurAttrStart + 1 )
end
if charindex ( N'>', @txt, @CurAttrStart + 1 ) = 0
begin
set @CurAttrEnd = charindex ( N' ', @txt, @CurAttrStart + 1 )
end
if @CurAttrStart = 0 or @CurAttrEnd = 0
beginbreak
end
if substring ( @txt, @CurAttrStart + 1, 1 ) not in ( N'"', N'''' ) and substring ( @txt, @CurAttrEnd + 1, 1 ) not in ( N'"', N'''' )
begin
set @txt = stuff ( @txt, @CurAttrStart + 1, 0, N'"' )
set @txt = stuff ( @txt, @CurAttrEnd + 1, 0, N'"' )
end
end
set @txt = replace ( @txt, N' ', N' ' )
set @txt = replace ( @txt, N' ', N' ' )
set @txt = replace ( @txt, N'&lang', N'' )
set @txt = replace ( @txt, N'&', N'' )
set @txt = replace ( @txt, N':', N'-' )
begin try
exec dbo.CheckDocTags @txt out
end try
begin catch
select @txt, error_message () [Описание ошибки], error_line () [Номер строки], error_procedure () [Процедура]
end catch
exec dbo.EliminateWrongAttr @txt out
set @xml = try_convert ( xml, @txt )
if @xml is null
begin
begin try
set @xml = cast ( @txt as xml )
end try
begin catch
set @err = error_message ()
end catch
select @txtPrev [Исходный текст], @txt [Преобразованный текст] , @err [Текст ошибки]
end
else
begin
begin try
exec dbo.ConvertXmlToTalbe @xml
end try
begin catch
select error_procedure () [Процедура], error_message () [Описание ошибки], error_line () [Номер строки], @xml [Документ]
end catch
end
end
До сих пор мы работали с html-таблицей. Но ее надо извлечь из html-документа. Для поиска таблицы мы, конечно, будем использовать имя тэга, то есть искать подстроки вида <TABLE. Но необходимо учесть, что внутри тэга TABLE может находиться другой такой тэг. Это вложенность может иметь нескольких уровней. Мы будем искать самый глубокий тэг TABLE и извлекать содержимое таблицы. Хранимая процедура dbo.FindTables решает задачу. Она в цикле извлекает все таблицы из документа и вызывает для таблицы хранимую процедуру dbo.MakeXmlFromTable.
if sessionproperty ( N'ansi_nulls' ) = 0
set ansi_nulls on
go
if sessionproperty ( N'quoted_identifier' ) = 0
set quoted_identifier on
go
@txt nvarchar ( max )set ansi_nulls on
go
if sessionproperty ( N'quoted_identifier' ) = 0
set quoted_identifier on
go
if object_id ( N'dbo.FindTables, N'P' ) is null
exec ( N'create proc dbo.FindTables as return 1' )
go
alter proc dbo.FindTables
(exec ( N'create proc dbo.FindTables as return 1' )
go
alter proc dbo.FindTables
)
as
begin
declare @StartTab int, @EndTab int, @StartNextTab int, @TabTxt nvarchar ( max ), @cnt int
set @StartTab = 1
set @cnt
= 1while @StartTab <> 0
begin
set @StartTab = charindex ( N'<table', @txt, case when @cnt = 1 then 0 else 1 end + @StartTab )
if @StartTab = 0
beginbreak
end
set @EndTab = charindex ( N'/table>', @txt, @StartTab + 1 )
set @StartNextTab = charindex ( N'<table', @txt, @StartTab + 1 )
if @StartNextTab <> 0
beginif @StartNextTab < @EndTab
begin
while @StartNextTab <> 0 and @StartNextTab < @EndTab
begin
set @StartTab = charindex ( N'<table', @txt, @StartTab + 1 )
set @StartNextTab = charindex ( N'<table', @txt, @StartTab + 1 )
end
end
end
set @EndTab = charindex ( N'/table>', @txt, @StartTab + 1 )
set @TabTxt = substring ( @txt, @StartTab, @EndTab + len ( N'/table>' ) - @StartTab )exec dbo.MakeXmlFromTab @TabTxt
set @cnt += 1
end
end
Вроде бы на этом все. Теперь можно протестировать работу группы процедур на примере сайта банка Москвы (http://www.bm.ru). Запустим такой код
declare @txt nvarchar ( max ) = dbo.HtmlCodeGet ( 'http://www.bm.ru' )
exec dbo.FindTables @txtВ результате получаем информацию о таблицах сайта в привычном для SQL Server-а виде:
Выше приведены только несколько последних таблиц сайта, чтобы не загромождать страницу большим скриншотом.
Комментариев нет:
Отправить комментарий