Три функции, предназначенные для извлечения из HTML-документа только нужной информации

Администраторам систем Windows периодически приходится заниматься извлечением конкретной информации из документов HTML. Такой документ может быть локальным файлом, страницей состояния устройства, подключенного к локальной сети, отчетом из базы данных, доступной через Web, или любой другой из тысячи типов страниц. В каждом из этих случаев мы обычно сталкиваемся с двумя проблемами использования данных. Первая из них состоит в подключении к нужной Web-странице и считывании содержащихся в ней данных. Дело в том, что если страница не является статическим файлом, доступным через какие-либо разделяемые ресурсы Windows или файловую систему в нашей сети, то мы не можем считывать ее содержимое, используя такие стандартные средства, как, например, объект Scripting.FileSystemObject. Кроме того, для доступа к устройству, обслуживающему интересующую Web-страницу, может потребоваться указать соответствующие имя пользователя и пароль. При этом после решения указанной проблемы мы сталкиваемся с еще более сложной задачей: как извлечь интересующую информацию из практически "сырого" (raw) HTML?

Обе проблемы могут быть решены с помощью стандартных компонентов Windows Script Host (WSH). Я представлю общую идею на примере демонстрационного сценария, с помощью которого из DSL-маршрутизатора извлекается Web-страница, из которой, в свою очередь, извлекаются данные о публичном (public) IP-адресе маршрутизатора. Затем из этого процесса будут выделены три универсальных функции, которые в дальнейшем можно использовать для извлечения информации из страниц различных типов.

Обращаю ваше внимание, что в данной статье словам "информация" и "данные" придаются разные значения. Под "информацией" здесь следует понимать те полезные сведения, которые нужно извлечь - в данном случае это сведения о публичном IP-адресе маршрутизатора. Если же используется термин "данные", то речь идет о содержимом страницы в "сыром" виде, которое содержит как "информацию", так и ненужный материал, который должен быть отсеян.

Извлечение данных

В состав Windows 2000 и последующих версий включены разнообразные компоненты, которые могут использоваться для извлечения Web-данных. Более ранние 32-разрядные версии Windows обычно имеют такую функциональность только в тех случаях, когда на них установлены обновления и пакеты Feature Pack начиная с 1999 года. Наиболее очевидный инструмент для извлечения данных из Web-страниц - Microsoft Internet Explorer (IE), но лично я его не использую. Конечно, существует много примеров применения IE в сценариях, и, тем не менее, он разработан в первую очередь для интерактивного отображения материала. В частности, проблемы появляются при использовании IE в неграфических сеансах; IE может стать причиной возникновения ошибок, блокирующих ход выполнения, а при работе с удаленными данными произвольного содержания весьма вероятны побочные эффекты. Следует также отметить, что поскольку IE автоматически обрабатывает и отображает дополнительный материал (в частности, встроенные изображения), это существенно замедляет его работу.

Я обычно использую для извлечения данных через HTTP-соединение другой компонент, а именно запросчик XMLHTTP (Microsoft XMLHTTP requester) из библиотеки msxml.dll. Сначала создается ссылка на объект-запросчик, как показано ниже:

Dim xml
Set xml = CreateObject ("Microsoft.XMLHTTP")

Работа с запросчиком XMLHTTP делится на три стадии: установка соединения, отправка запроса и получение ответа. Для данного компонента открытие соединения подразумевает также детальное описание его параметров. Полный синтаксис вызова метода open объекта Microsoft.XMLHTTP выглядит следующим образом (в квадратных скобках приведены необязательные аргументы):

open(method, url, [async],   [user], [pass])

Здесь method - строковый аргумент, описывающий тип выполняемого запроса. В случае HTTP-соединений он обычно имеет значение "GET". Аргумент url также является строковым параметром, в котором, в случае обращения к удаленным данным, должен полностью определяться корректный путь URL. При работе с локальными файлами данный аргумент тоже используется, но в этом случае он представляет собой полный путь к файлу, с которым предполагается работать (при этом префикс file:// в описании пути использовать не нужно).

В нашем случае требуется тот URL, который отображается в окне браузера при просмотре нужной страницы конфигурации на удаленной системе. В качестве примера будет рассмотрено взаимодействие с маршрутизатором HomePortal 1800HG DSL, который часто используется в квартирах и небольших офисах для организации доступа к Internet. Публичный IP-адрес данного устройства отображается на странице http://10.1.1.1/?PAGE=B01, соответственно, именно этот URL и будет использоваться.

Необязательный аргумент async служит для того, чтобы указать запросчику, должен ли он ожидать отклика (синхронный режим, ему соответствует значение False) или переходить к следующей строке кода сразу после отправки запроса (асинхронный режим - значение True). Если данный аргумент пропущен, то по умолчанию берется значение True, но в рассматриваемом примере это недопустимо, так как после отправки запроса необходимо, чтобы сценарий дождался ответа, поскольку в сценарии сразу вслед за отправкой запроса выполняется обработка его результатов. Итак, в данном случае должно быть задано значение False.

Следующие два аргумента будут полезны в случае обращения к ресурсам с ограниченным доступом, но жестко задавать их в сценариях следует с осторожностью. Если для доступа к ресурсу должны указываться имя пользователя и пароль, то они могут быть заданы, соответственно, в виде аргументов user и pass, что позволяет избежать ошибки аутентификации при выполнении запроса. Но если для доступа к интересующему ресурсу аутентификация не требуется, тогда эти аргументы задавать не нужно, или можно указать пустую строку - либо с помощью vbNullString, либо через символ двойных кавычек (""). Теперь у нас есть вся необходимая информация для установки соединения, и мы добавляем в сценарий следующую строку:

xml.open "GET", "http://10.1.1.1/?PAGE=B01", False

Таким образом, мы сконфигурировали и установили соединение, но сам запрос еще не сделали. Для этого воспользуемся методом send:

xml.send

По окончании выполнения запроса происходит обращение к полученным данным, для чего используется свойство responseText запросчика:

data = xml.responseText

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

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

Internet Address: www.xxx.yyy.zzz

Видно, что в полученных в результате выполнения запроса данных присутствует значительный уровень "шума", соответственно, для выделения интересующей нас информации этот шум должен быть отфильтрован. Иначе говоря, мы имеем более 200 строк текста и примерно 1250 слов, и во всей этой массе требуется найти только то слово, которое нас интересует.

Фильтрация шумов

Было бы хорошо иметь механизм для быстрого отсеивания этих шумов. В принципе, тэги HTML и XML можно отфильтровать, загрузив полученные данные в объект XmlDocument или HtmlDocument, после чего выполнить запрос, возвращающий только текст документа. Однако если данные не вполне соответствуют требованиям используемого объекта, это может привести к проблемам.

В целом, на мой взгляд, в данном подходе больше риска, чем преимуществ, и вот почему. Объект XmlDocument может работать с конфигурационной страницей нашего маршрутизатора, но если она не будет иметь полностью корректный формат документа XML, то процесс обработки даст сбой. Что же касается объекта HtmlDocument, то, в принципе, он предоставляет прекрасные возможности фильтрации нежелательного содержимого, но с ним связан и значительный риск, поскольку работа нашего сценария будет прервана. Если страница содержит нерегулярные данные - причем это может быть и такое содержимое, которое без ошибок обрабатывается в IE - то в этом случае объект HtmlDocument может сгенерировать окно ошибки, блокирующее интерфейс. Причем сам сценарий может, тем не менее, продолжить работу на данной странице с виртуальным документом, что в результате приведет к нестабильной работе.

На мой взгляд, наилучший способ состоит в использовании регулярных выражений. С помощью короткого регулярного выражения можно получить сведения об IP-адресе, хотя может показаться предпочтительным задействовать объект XmlDocument. Но это было бы не совсем корректно, поскольку данный способ, наиболее удобный для описываемого случая, совершенно бесполезен для других задач. В то же время, фильтрация стандартных Web-данных с помощью регулярных выражений позволит привести их к такому виду, при котором с помощью более простого регулярного выражения или обычного поиска строковых значений можно будет быстро найти интересующую нас информацию.

Итак, сначала требуется создать ссылку на механизм работы с регулярными выражениями в VBScript. Поскольку интересующий нас фрагмент может быть обнаружен в нескольких строках извлеченных данных, мы должны задействовать свойства Multiline и Global:

Dim rx
Set rx = New RegExp
rx.Multiline = True
rx.Global = True

Первый шаг в процессе выделения целевой информации состоит в удалении из общего объема данных всех тэгов HTML. Поскольку тэги HTML и XML всегда начинаются и заканчиваются символами угловых скобок (<>), их можно легко идентифицировать, применив следующий шаблон (pattern):

rx.Pattern = "<[^>]+>"

Те, кто имеет опыт работы с регулярными выражениями, уже поняли, что этот шаблон определяет поиск строк, начинающихся символом "<" и заканчивающихся символом ">". Однако многих может сбить с толку часть выражения, находящаяся внутри угловых скобок (т.е. [^>]+). Когда в выражении используется символ вставки (^), заключенный между символами набора знаков ([) и (]), это предписывает выявлять совпадения с любыми символами, не входящими в данный набор. Знак плюс (+) более привычен для тех, кто пользуется регулярными выражениями; он означает выявление совпадений с последовательностью из одного или более знаков из данного набора. Следовательно, выражение [^>]+ обозначает выявление совпадения с любым символом, кроме символа >. Таким образом, полное описание шаблона <[^>]+> определяет выявление любых последовательностей, начинающихся с символа <, заканчивающихся символом >, но не содержащих символ >.

После того как все тэги были выявлены, остается просто удалить их. Для этого используем регулярное выражение в режиме замены, подставляя вместо тэгов пустые строки:

data = rx.Replace(data, vbNullString)

Если взглянуть на стандартную Web-страницу после выполнения этой операции, заметим, что на ней осталось значительное количество данных, причем большая их часть является пустыми, т.е. комбинацией символов табуляции, стандартных пробелов и символов окончания строки. Также встречаются и специальные символы HTML. И хотя в рассматриваемом здесь примере Web-страницы конфигурации маршрутизатора их наличие не является принципиальным, тем не менее, имеет смысл очистить страницу от одного из этих символов, поскольку это позволит упростить последующую процедуру поиска: речь идет о специальном символе неразрывного пробела, обычно обозначаемом на Web-страницах как " ". В данном случае для замены этого символа можно снова воспользоваться возможностями регулярных выражений, но проще использовать функцию замены символов из VBScript. С помощью приведенной ниже команды каждый из этих спецсимволов будет заменен символом пробела:

data = Replace(data, " ", " ", 1, -1, _1)

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

rx.Pattern = "s+"

Затем заменим каждое из найденных совпадений символом единичного пробела:

data = rx.Replace(data, " ")

Значимая информация

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

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

Рассмотрим, например, словосочетание "Internet Address: " (обратите внимание, что в данном примере адрес имеет заведомо некорректное значение 256.261.381.125). Непосредственно перед значением адреса находятся символы двоеточия и пробела, но данный фрагмент не является на рассматриваемой странице уникальным. Если мы передвинемся по тексту назад, то получим набор символов "Address: ", который опять-таки не уникален. А вот словосочетание "Internet Address: " встречается на странице только один раз. Соответственно, если расщепить данные по разделителю "Internet Address: ", то получится массив, состоящий из двух строковых элементов, один из которых будет содержать текст, расположенный до разделителя "Internet Address: ", а другой - текст после разделителя. В данном случае нас интересует второй элемент, который имеет индекс 1, поэтому поступим следующим образом:

data = Split(data, "Internet Address: ")(1)

Теперь наша задача проста. По тексту сразу после интересующей нас информации следует символ пробела, а поскольку в самом значении IP-адреса пробелов быть не может, можно расщепить данные по символу пробела. Сохраним первую строку (она имеет индекс 0) из сформированного выше с помощью оператора Split массива с помощью приведенного ниже оператора:

data = Split(data, " ")(0)

и расположим ее слева от значения 256.261.381.125.

Чтобы убедиться в том, что данная методика пригодна для повторного использования, взгляните на Листинг 1. Каждый значимый этап обработки данных - получение данных страницы, удаление тэгов и дополнительных пробелов, локализация интересующей строковой информации - здесь реализован в виде отдельной функции (см. фрагмент с меткой B). После определения этих функций сам процесс извлечения из страницы информации об IP-адресе сводится к трем строкам кода, что иллюстрирует фрагмент с меткой A.

Если просмотреть приведенный ниже код, увидим, что в функции GetSubString используется встроенная функция Trim языка VBScript, хотя непосредственно в нашем примере она не использовалась. Функция Trim служит для удаления пробелов в начале и в конце той текстовой строки, к которой она применяется. Я включил ее в функцию GetSubString для того, чтобы можно было не принимать во внимание возможные пробелы в начале и в конце тех строк, которые используются для извлечения данных.

Наличие этих функций позволит упростить процесс адаптации данного сценария под конкретные задачи. В частности, если вы имеете опыт извлечения нужного материала из страниц с помощью регулярных выражений и предпочитаете делать это каким-либо привычным способом, тогда вы можете пользоваться только функцией GetWebXml для извлечения данных, а потом обрабатывать их нужным образом. Если же вы предпочитаете использовать готовые решения, но имеете некоторые специфические требования (например, необходимо извлечь не одну, а несколько строк), то в этом случае можно задействовать функцию CleanTaggedText и игнорировать функцию GetSubString. И, наконец, если из всей страницы требуется извлечь только одну строку и получить соответствующий уникальный текст перед самим значением и после него, тогда можно использовать все три функции - так, как это делаю я.


Листинг 1. Сценарий для извлечения информации из Web-страницы.
' Начало фрагмента A
Dim data
data = GetWebXml("http://10.1.1.1/?PAGE=B01", "", "")
data = CleanTaggedText(data)
WScript.Echo GetSubString(data, "Internet Address:", " ")
' Конец фрагмента A
' Начало фрагмента B
Function GetWebXml(ByVal url, Byval name, ByVal pass)
  Dim xml
  Set xml = CreateObject("Microsoft.XMLHTTP")
  xml.open "GET", url, False, name, pass
  xml.send
  GetWebXml = xml.responseText
End Function
Function CleanTaggedText(ByVal s)
  s = Replace(s, " ", " ", 1, -1, vbTextCompare)
  Dim rx
  Set rx = New RegExp
  rx.Multiline = True
  rx.Global = True
  rx.Pattern = "<[^>]+>"
  s = rx.Replace(s, vbNullString)
  rx.Pattern = "s+"
  CleanTaggedText = Trim(rx.Replace(s, " "))
End Function
Function GetSubString(ByVal s, ByVal before, ByVal after)
  s = Trim(Split(s, before)(1))
  GetSubString = Trim(Split(s, after)(0))
End Function
' Конец фрагмента B