вторник, 5 июня 2018 г.

Поговорим об XPathNavigator

  1. О языке XPath
  2. Преимущества XPathNavigator
  3. Создаем собственный XPathNavigator
  4. Расширение языка собственной функцией
  5. Тестирование
  6. Использование XSLT

О языке XPath



XPath – прекрасный язык навигации по XML-документу. Его удобно использовать как для отбора узлов в документе XML, так и как составную часть других языков, таких как: XLink, XSLT или XQuery. Несмотря на то, что для отбора узлов платформа .NET Framework располагает и другими средствами, такими как LINQ to XML, тем не менее у XPath все-таки сохранилась своя ниша для использования, неслучайно даже для классов LINQ to XML поддержка XPath также реализована, хоть и в виде методов-расширений. К преимуществам XPath можно отнести то, что код, использующий этот язык зачастую оказывается более коротким и самое главное – что выражения, написанные на этом языке можно передавать в виде текста, что позволяет не закладывать структуру обрабатываемого документа в код, а вместо этого держать выражения где-то отдельно и изменять их в случае необходимости, не меняя при этом код программы.

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

Преимущества XPathNavigator



System.Xml.XPath.XPathNavigator как раз и представляет из себя механизм, позволяющий использовать XPath для любых объектов. Платформа .Net Framework предоставляет возможность наследовать этот абстрактный класс и таким образом реализовывать поддержку XML-технологий для различных объектов. Конечно, есть и другие инструменты, позволяющие использовать XML API для разных объектов. Например, можно реализовать собственный XmlReader или обойти объект рекурсивно и создать XML-слепок объекта (под слепком я подразумеваю XML-документ, имеющий такую же структуру, как и исследуемый объект). Но все эти способы имеют свои недостатки. XmlReader движется поступательно и не видит контекста, а «слепок» понятия не имеет об изменениях в объекте, из-за чего приходится создавать новый «слепок» всякий раз, когда объект мог измениться, а из него нужно получить данные. Ну и кроме того «слепок» - это дополнительный расход памяти. XPathNavigator лишен этих недостатков, поскольку умеет двигаться по оригинальному объекту, клонировать себя и двигаться в разных направлениях.

Создаем собственный XPathNavigator



Мы будем создавать XPathNavigator для узлов HTML-документа из библиотеки mshtml. Причины такого выбора достаточно просты:
  1. HTML очень похож на XML, поэтому работа нашего навигатора будет наглядной.
  2. В реализации есть некоторые сложности, стало быть преодолев их, можно лучше понять, как работать с навигатором.
  3. Такой навигатор имеет практическую пользу, поскольку его можно будет использовать и с WebBrowser’ом и в случае непосредственной работы с библиотекой mshtml.

Для реализации навигатора достаточно переопределить только абстрактные методы класса XPathNavigator. Фактически задача сводится к тому, чтобы «объяснить» навигатору какую операцию надо произвести с узлом документа, чтобы выполнить стандартное для навигатора действие. Операции эти просты и понятны: перейти к родителю, к первому атрибуту, к первому потомку, к следующему брату, клонировать себя и т. п. В то же время нам понадобятся некоторые дополнительные приемы, которые позволят нам адаптировать логику поведения навигатора к особенностям библиотеки mshtml.

Первая сложность, с которой нам придется справиться – это то, что с точки зрения навигатора атрибуты – это обычные узлы документа, а с точки зрения библиотеки mshtml – это не совсем так. Под «не совсем так» я понимаю то обстоятельство, что в данной библиотеке объекты узлов-атрибутов не реализуют интерфейс IHTMLDOMNode, а именно с ним мы и будем работать. Поэтому для атрибутов нам придется создать адаптер – класс который будет реализовывать этот интрефейс и переадресовывать его вызовы узлу-атрибуту. Большинство методов реализовывать необязательно, поскольку для атрибутов они не имеют смысла, так что можно сделать так (естественно библиотека mshtml должна быть подключена к проекту).

Кликните здесь для просмотра всего текста
Код vbnet Выделить
Imports mshtml
 
Public Class AttributeNode
    Implements mshtml.IHTMLDOMNode
 
    Dim _node As mshtml.IHTMLDOMAttribute
 
    Public Sub New(node As mshtml.IHTMLDOMAttribute)
        Me._node = node
    End Sub
 
    Public ReadOnly Property attributes As Object Implements IHTMLDOMNode.attributes
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public ReadOnly Property childNodes As Object Implements IHTMLDOMNode.childNodes
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public ReadOnly Property firstChild As IHTMLDOMNode Implements IHTMLDOMNode.firstChild
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public ReadOnly Property lastChild As IHTMLDOMNode Implements IHTMLDOMNode.lastChild
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public ReadOnly Property nextSibling As IHTMLDOMNode Implements IHTMLDOMNode.nextSibling
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public Property Node As IHTMLDOMAttribute
        Get
            Return _node
        End Get
        Set(value As IHTMLDOMAttribute)
            _node = value
        End Set
    End Property
 
    Public ReadOnly Property nodeName As String Implements IHTMLDOMNode.nodeName
        Get
            Return _node.nodeName
        End Get
    End Property
 
    Public ReadOnly Property nodeType As Integer Implements IHTMLDOMNode.nodeType
        Get
            Return _node.nodeType
        End Get
    End Property
 
    Public Property nodeValue As Object Implements IHTMLDOMNode.nodeValue
        Get
            Return Me._node.nodeValue
        End Get
        Set(value As Object)
            Me._node.nodeValue = value
        End Set
    End Property
 
    Public ReadOnly Property parentNode As IHTMLDOMNode Implements IHTMLDOMNode.parentNode
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public ReadOnly Property previousSibling As IHTMLDOMNode Implements IHTMLDOMNode.previousSibling
        Get
            Throw New NotImplementedException()
        End Get
    End Property
 
    Public Function appendChild(newChild As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.appendChild
        Throw New NotImplementedException()
    End Function
 
    Public Function cloneNode(fDeep As Boolean) As IHTMLDOMNode Implements IHTMLDOMNode.cloneNode
        Throw New NotImplementedException()
    End Function
 
    Public Function hasChildNodes() As Boolean Implements IHTMLDOMNode.hasChildNodes
        Throw New NotImplementedException()
    End Function
 
    Public Function insertBefore(newChild As IHTMLDOMNode, Optional refChild As Object = Nothing) As IHTMLDOMNode Implements IHTMLDOMNode.insertBefore
        Throw New NotImplementedException()
    End Function
 
    Public Function removeChild(oldChild As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.removeChild
        Throw New NotImplementedException()
    End Function
 
    Public Function removeNode(Optional fDeep As Boolean = False) As IHTMLDOMNode Implements IHTMLDOMNode.removeNode
        Throw New NotImplementedException()
    End Function
 
    Public Function replaceChild(newChild As IHTMLDOMNode, oldChild As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.replaceChild
        Throw New NotImplementedException()
    End Function
 
    Public Function replaceNode(replacement As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.replaceNode
        Throw New NotImplementedException()
    End Function
 
    Public Function swapNode(otherNode As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.swapNode
        Throw New NotImplementedException()
    End Function
End Class


Следующая сложность в том, что некоторые типы элементов HTML в библиотеке mshtml ведут себя немного не так как остальные элементы, что для XML – неприемлемо. В частности, я говорю о таких элементах, как script, title или style. Необычность их в том, что текст, содержащийся внутри этих элементов, не описывается как дочерний текстовый узел, а в первых двух элементах его можно получить из свойства text, а у последнего есть свойство styleSheet, возвращающее объект стиля, текст которого можно получить из свойства cssText этого объекта. Нам же, для того, чтобы документ в результате имел первоначальный вид, потребуется объект текстового узла, содержащего нужный текст. Поэтому придется создать класс текстового узла для решения этой проблемы, ну и, конечно, реализовать в нем интерфейс IHTMLDOMNode.

Кликните здесь для просмотра всего текста
Код vbnet Выделить
Imports mshtml
 
Public Class TextNode
    Implements IHTMLDOMNode
 
    Dim _data As String
    Dim _parent As IHTMLDOMNode
 
    Public Sub New(data As String, parent As IHTMLDOMNode)
        _data = data
        _parent = parent
    End Sub
 
    Public ReadOnly Property attributes As Object Implements IHTMLDOMNode.attributes
        Get
            Return Nothing
        End Get
    End Property
 
    Public ReadOnly Property childNodes As Object Implements IHTMLDOMNode.childNodes
        Get
            Return Nothing
        End Get
    End Property
 
    Public ReadOnly Property firstChild As IHTMLDOMNode Implements IHTMLDOMNode.firstChild
        Get
            Return Nothing
        End Get
    End Property
 
    Public ReadOnly Property lastChild As IHTMLDOMNode Implements IHTMLDOMNode.lastChild
        Get
            Return Nothing
        End Get
    End Property
 
    Public ReadOnly Property nextSibling As IHTMLDOMNode Implements IHTMLDOMNode.nextSibling
        Get
            Return Nothing
        End Get
    End Property
 
    Public ReadOnly Property nodeName As String Implements IHTMLDOMNode.nodeName
        Get
            Return "#text"
        End Get
    End Property
 
    Public ReadOnly Property nodeType As Integer Implements IHTMLDOMNode.nodeType
        Get
            Return 3
        End Get
    End Property
 
    Public Property nodeValue As Object Implements IHTMLDOMNode.nodeValue
        Get
            Return _data
        End Get
        Set(value As Object)
            _data = value
        End Set
    End Property
 
    Public ReadOnly Property parentNode As IHTMLDOMNode Implements IHTMLDOMNode.parentNode
        Get
            Return _parent
        End Get
    End Property
 
    Public ReadOnly Property previousSibling As IHTMLDOMNode Implements IHTMLDOMNode.previousSibling
        Get
            Return Nothing
        End Get
    End Property
 
    Public Function appendChild(newChild As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.appendChild
        Throw New NotImplementedException()
    End Function
 
    Public Function cloneNode(fDeep As Boolean) As IHTMLDOMNode Implements IHTMLDOMNode.cloneNode
        Throw New NotImplementedException()
    End Function
 
    Public Function hasChildNodes() As Boolean Implements IHTMLDOMNode.hasChildNodes
        Return False
    End Function
 
    Public Function insertBefore(newChild As IHTMLDOMNode, Optional refChild As Object = Nothing) As IHTMLDOMNode Implements IHTMLDOMNode.insertBefore
        Throw New NotImplementedException()
    End Function
 
    Public Function removeChild(oldChild As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.removeChild
        Throw New NotImplementedException()
    End Function
 
    Public Function removeNode(Optional fDeep As Boolean = False) As IHTMLDOMNode Implements IHTMLDOMNode.removeNode
        Throw New NotImplementedException()
    End Function
 
    Public Function replaceChild(newChild As IHTMLDOMNode, oldChild As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.replaceChild
        Throw New NotImplementedException()
    End Function
 
    Public Function replaceNode(replacement As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.replaceNode
        Throw New NotImplementedException()
    End Function
 
    Public Function swapNode(otherNode As IHTMLDOMNode) As IHTMLDOMNode Implements IHTMLDOMNode.swapNode
        Throw New NotImplementedException()
    End Function
End Class


Теперь код самого навигатора.
Кликните здесь для просмотра всего текста
Код vbnet Выделить
Imports System.Xml.XPath
Imports mshtml
 
Public Class HNavigator
    Inherits XPathNavigator
 
    Public Sub New(node As IHTMLDOMNode)
        Me.node = node
    End Sub
 
    Dim isAttribute As Boolean
    Dim attIndex As Integer = -1
    Dim attributes As New List(Of AttributeNode)
 
    Dim node As IHTMLDOMNode
    Public ReadOnly Property CurrentNode As IHTMLDOMNode
        Get
            If isAttribute Then
                Return attributes(attIndex)
            End If
            Return node
        End Get
    End Property
 
    Private Sub InitializeAttributes()
        attributes.Clear()
        Dim atts As IHTMLAttributeCollection = node.attributes
        For Each att As IHTMLDOMAttribute In atts
            Dim value = att.nodeValue
            If Not IsDBNull(value) AndAlso Not IsNothing(value) AndAlso att.specified Then
                attributes.Add(New AttributeNode(att))
            End If
        Next
    End Sub
 
    Function SubstringAfter(strobj As String, separator As String) As String
        If strobj.Contains(separator) Then
            Return strobj.Substring(strobj.IndexOf(separator) + separator.Length)
        End If
        Return strobj
    End Function
 
#Region "XPathNavigator abstracts"
    Public Overrides ReadOnly Property BaseURI As String
        Get
            Return ""
        End Get
    End Property
 
    Public Overrides Function Clone() As XPathNavigator
        Return New HNavigator(Me.node) With {.isAttribute = isAttribute, .attIndex = attIndex, ._nameTable = _nameTable}
    End Function
 
    Public Overrides ReadOnly Property IsEmptyElement As Boolean
        Get
            Return Me.CurrentNode.childNodes.length = 0
        End Get
    End Property
 
    Public Overrides Function IsSamePosition(other As XPathNavigator) As Boolean
        If TypeOf CurrentNode Is TextNode Then
            Return CurrentNode.parentNode Is CType(other, HNavigator).CurrentNode.parentNode
        End If
        Return CType(other, HNavigator).CurrentNode Is Me.CurrentNode
    End Function
 
    Public Overrides ReadOnly Property LocalName As String
        Get
            If isAttribute AndAlso attributes.Count > attIndex Then
                Return Xml.XmlConvert.EncodeName(SubstringAfter(attributes(attIndex).nodeName.ToString, ":"))
            ElseIf CurrentNode.nodeType = 1 Then
                Return Xml.XmlConvert.EncodeName(SubstringAfter(Me.CurrentNode.nodeName.ToString, ":")) '.ToLower()
            Else
                Return CurrentNode.nodeName
            End If
        End Get
    End Property
 
    Public Overrides Function MoveTo(other As XPathNavigator) As Boolean
        Dim onav = CType(other, HNavigator)
        If onav.CurrentNode IsNot Nothing Then
            Me.node = onav.node
            Me.isAttribute = onav.isAttribute
            Me.attIndex = onav.attIndex
            Return True
        End If
        Return False
    End Function
 
    Public Overrides Function MoveToFirstAttribute() As Boolean
        InitializeAttributes()
        If attributes.Count > 0 Then
            isAttribute = True
            attIndex = 0
            Return True
        End If
        Return False
    End Function
 
    Public Overrides Function MoveToFirstChild() As Boolean
        isAttribute = False
        Dim textElements = {"TITLE", "SCRIPT"}
        Dim first = CurrentNode.firstChild
        If CurrentNode.nodeName = "STYLE" Then
            Me.node = New TextNode(CType(CurrentNode, HTMLStyle).styleSheet.cssText, CurrentNode)
            Return True
        ElseIf textElements.Contains(CurrentNode.nodeName) Then
            Dim text = CurrentNode.GetType().GetProperty("text")
            Me.node = New TextNode(text.GetValue(CurrentNode), CurrentNode)
            Return True
        ElseIf first IsNot Nothing Then
            Me.node = first
            Return True
        End If
        Return False
    End Function
 
    Public Overloads Overrides Function MoveToFirstNamespace(namespaceScope As XPathNamespaceScope) As Boolean
        Return False
    End Function
 
    Public Overrides Function MoveToId(id As String) As Boolean
        Dim doc As HTMLDocument = CType(Me.CurrentNode, IHTMLDOMNode2).ownerDocument
        Dim el = doc.getElementById(id)
        If el IsNot Nothing Then
            Me.node = el
            Me.isAttribute = False
            Return True
        End If
        Return False
    End Function
 
    Public Overloads Overrides Function MoveToNext() As Boolean
        Dim nextsibl = Me.CurrentNode.nextSibling
        If nextsibl IsNot Nothing Then
            Me.node = nextsibl
            Return True
        End If
        Return False
    End Function
 
    Public Overrides Function MoveToNextAttribute() As Boolean
        If attributes.Count > attIndex + 1 Then
            attIndex += 1
            Return True
        End If
 
        Return False
    End Function
 
    Public Overloads Overrides Function MoveToNextNamespace(namespaceScope As XPathNamespaceScope) As Boolean
        Return False
    End Function
 
    Public Overrides Function MoveToParent() As Boolean
        If isAttribute Then
            isAttribute = False
            Return True
        End If
        Dim parent = CurrentNode.parentNode
        If parent IsNot Nothing Then
            Me.node = parent
            Return True
        End If
        Return False
    End Function
 
    Public Overrides Function MoveToPrevious() As Boolean
        Dim prevsibl = Me.CurrentNode.previousSibling
        If prevsibl IsNot Nothing Then
            Me.node = prevsibl
            Return True
        End If
        Return False
    End Function
 
    Public Overrides ReadOnly Property Name As String
        Get
            Return LocalName
        End Get
    End Property
 
    Public Overrides ReadOnly Property NamespaceURI As String
        Get
            Return ""
        End Get
    End Property
 
 
    Dim _nameTable As Xml.NameTable
    Public Overrides ReadOnly Property NameTable As Xml.XmlNameTable
        Get
            If _nameTable IsNot Nothing Then _nameTable = New Xml.NameTable
            Return _nameTable
        End Get
    End Property
 
    Public Overrides ReadOnly Property NodeType As XPathNodeType
        Get
            If Me.isAttribute Then
                Return XPathNodeType.Attribute
            End If
            Select Case Me.CurrentNode.nodeType
                Case 1
                    Return XPathNodeType.Element
                Case 2
                    Return XPathNodeType.Attribute
                Case 3
                    Return XPathNodeType.Text
                Case 8
                    Return XPathNodeType.Comment
                Case Else
                    Return CurrentNode.nodeType
            End Select
        End Get
    End Property
 
    Public Overrides ReadOnly Property Prefix As String
        Get
            Return ""
        End Get
    End Property
 
    Public Overrides ReadOnly Property Value As String
        Get
            Return Me.CurrentNode.nodeValue.ToString
        End Get
    End Property
 
#End Region
 
End Class

Опишу несколько вопросов, на которые следует обратить внимание.
Поскольку, как уже говорилось, атрибуты не являются полноценными узлами в mshtml, а элемент содержащий атрибут, не является для этого узла родительским и вообще атрибут не содержит ссылку на элемент, в котором он определен, то нам придется позаботиться о том, чтобы с атрибута можно было вернуться к элементу. В навигаторе у нас есть поле node, содержащее текущий узел, но присваивать этому полю ссылки на атрибуты мы не будем. Вместо этого у нас будет коллекция атрибутов attributes, булево поле isAttribute и целочисленное поле attIndex. Таким образом если навигатор находится на узле атрибута, то на самом деле мы его размещаем на элементе, содержащем этот атрибут, поле isAttribute имеет значение True, а attIndex содержит индекс текущего атрибута в коллекции attributes. Для удобства доступа мы создали свойство CurrentAttribute, которое в зависимости от значений этих полей будет возвращать либо node, либо один из его атрибутов.
Код vbnet Выделить
    Public Sub New(node As IHTMLDOMNode)
        Me.node = node
    End Sub
 
    Dim isAttribute As Boolean
    Dim attIndex As Integer = -1
    Dim attributes As New List(Of AttributeNode)
 
    Dim node As IHTMLDOMNode
    Public ReadOnly Property CurrentNode As IHTMLDOMNode
        Get
            If isAttribute Then
                Return attributes(attIndex)
            End If
            Return node
        End Get
    End Property
 
В коллекцию attributes мы загружаем атрибуты из аналогичной коллекции HTML-элемента. Это вообще сделать очень полезно, поскольку несколько увеличивает производительность навигатора. Но в данном случае мы еще и отбираем только те атрибуты, которые либо явно заданы в документе, либо добавлены элементу с помощью скрипта. То есть те, у которых свойство specified имеет значение true. 
    Private Sub InitializeAttributes()
        attributes.Clear()
        Dim atts As IHTMLAttributeCollection = node.attributes
        For Each att As IHTMLDOMAttribute In atts
            Dim value = att.nodeValue
            If Not IsDBNull(value) AndAlso Not IsNothing(value) AndAlso att.specified Then
                attributes.Add(New AttributeNode(att))
            End If
        Next
    End Sub
Я здесь максимально упростил все, что связано с пространствами имен XML, то есть префикс и пространство имен всегда будут пустой строкой, имя узла будет совпадать с локальным именем, а локальное имя получается удалением префикса, если таковой у узла имеется. Кроме того, HTML позволяет создавать имена, недопустимые в XML, поэтому при вычислении локального имени пришлось немного подстраховаться. Кроме того, имена элементов всегда возвращаются в верхнем регистре. Если это нужно изменить, например возвращать их всегда в нижнем регистре или если нужно сделать эту характеристику опциональной, то сделать это надо именно здесь (раскомментировать '.ToLower()). Правда это негативно сказывается на производительности.
Код vbnet Выделить
    Public Overrides ReadOnly Property LocalName As String
        Get
            If isAttribute AndAlso attributes.Count > attIndex Then
                Return Xml.XmlConvert.EncodeName(SubstringAfter(attributes(attIndex).nodeName.ToString, ":"))
            ElseIf CurrentNode.nodeType = 1 Then
                Return Xml.XmlConvert.EncodeName(SubstringAfter(Me.CurrentNode.nodeName.ToString, ":")) '.ToLower()
            Else
                Return CurrentNode.nodeName
            End If
        End Get
    End Property
В методе MoveToFirstChild используется созданный ранее TextNode. Отдельно рассматриваются случаи, когда содержимое элемента возвращает свойство text (элементы TITLE и SCRIPT, возможно есть и другие, тогда их просто надо добавить в массив textElements) и отдельно элемент SCRIPT.
Код vbnet Выделить
    Public Overrides Function MoveToFirstChild() As Boolean
        isAttribute = False
        Dim textElements = {"TITLE", "SCRIPT"}
        Dim first = CurrentNode.firstChild
        If CurrentNode.nodeName = "STYLE" Then
            Me.node = New TextNode(CType(CurrentNode, HTMLStyle).styleSheet.cssText, CurrentNode)
            Return True
        ElseIf textElements.Contains(CurrentNode.nodeName) Then
            Dim text = CurrentNode.GetType().GetProperty("text")
            Me.node = New TextNode(text.GetValue(CurrentNode), CurrentNode)
            Return True
        ElseIf first IsNot Nothing Then
            Me.node = first
            Return True
        End If
        Return False
    End Function
Естественно, использование таких узлов, которые в исходном документе узлами не является, влечет за собой необходимость учитывать это обстоятельство и при реализации других методов. В частности, метод IsSamePosition для таких узлов применяет иную логику, нежели для всех остальных
Код vbnet Выделить
    Public Overrides Function IsSamePosition(other As XPathNavigator) As Boolean
        If TypeOf CurrentNode Is TextNode Then
            Return CurrentNode.parentNode Is CType(other, HNavigator).CurrentNode.parentNode
        End If
        Return CType(other, HNavigator).CurrentNode Is Me.CurrentNode
    End Function
В остальном, я думаю, реализация навигатора более-менее понятна.

Расширение языка собственной функцией



Когда я писал об атрибутах, я упомянул о том, что в коллекцию добавляются только те атрибуты, которые в документе объявлены явно либо значение им присвоено во время исполнения скрипта. В этой связи неплохо было бы иметь возможность прямо в выражениях XPath запрашивать те или иные свойства узла с возможностью их использовать при формировании результата или в фильтрах выражений. Для решения этой задачи добавим собственную функцию, которую мы сможем использовать в выражениях. Итак, нам нужна функция, которой мы сможем передавать выражение XPath и имя свойства, а она будет возвращать значение этого свойства для узла, которое возвращает выражение XPath. Кроме того, если функция получает только имя свойства, то в качестве узла она будет использовать узел контекста.
Для создания собственной функции нам потребуется создать класс, реализующий интерфейс System.Xml.Xsl.IXsltContextFunction
Кликните здесь для просмотра всего текста
Код vbnet Выделить
Imports System.Xml.XPath
Imports System.Xml.Xsl
 
Public Class GetPropertyExtensionFunction
    Implements IXsltContextFunction
 
    Public ReadOnly Property ArgTypes As XPathResultType() Implements IXsltContextFunction.ArgTypes
        Get
            Return New XPathResultType() {XPathResultType.NodeSet, XPathResultType.String}
        End Get
    End Property
 
    Public ReadOnly Property Maxargs As Integer Implements IXsltContextFunction.Maxargs
        Get
            Return 2
        End Get
    End Property
 
    Public ReadOnly Property Minargs As Integer Implements IXsltContextFunction.Minargs
        Get
            Return 1
        End Get
    End Property
 
    Public ReadOnly Property ReturnType As XPathResultType Implements IXsltContextFunction.ReturnType
        Get
            Return XPathResultType.Any
        End Get
    End Property
 
    Public Function Invoke(xsltContext As XsltContext, args() As Object, docContext As XPathNavigator) As Object Implements IXsltContextFunction.Invoke
        Dim node As mshtml.IHTMLDOMNode
        Dim propName As String
        If args.Length = 1 Then
            node = CType(docContext, HNavigator).CurrentNode
            propName = args(0).ToString()
        Else
            Dim nodeSet As XPathNodeIterator = CType(args(0), XPathNodeIterator)
            node = CType(nodeSet(0), HNavigator).CurrentNode
            propName = args(1).ToString
        End If
        Dim prop = node.GetType.GetProperty(propName)
        Return prop.GetValue(node)
    End Function
End Class

Основная логика нашей функции заключена в методе Invoke, так что скажу о ней пару слов. В тех случаях, когда функции передается два аргумента и первый – выражение, возвращающее набор узлов, мы берем из этого набора первый узел и работаем с ним. Набор узлов в коде представлен типом XPathNodeIterator, а отдельный узел – как раз XPathNavigator, в нашем случае это будет как раз созданный нами навигатор, то есть HNavigator, являющийся подтипом XPathNavigator. Поэтому, в случае если функция приняла два аргумента, то нужный узел мы извлекаем из первого из них. В случае же, когда функция приняла только один аргумент, нужный нам узел мы получим из последнего аргумента метода Invoke, это и будет узел контекста.
Кроме того, нам для использования этой функции нам нужно создать класс, унаследованный от System.Xml.Xsl.XsltContext.
Кликните здесь для просмотра всего текста
Код vbnet Выделить
Imports System.Xml.XPath
Imports System.Xml.Xsl
Public Class XContext
    Inherits XsltContext
 
    Public Overrides ReadOnly Property Whitespace As Boolean
        Get
            Return True
        End Get
    End Property
 
    Public Overrides Function CompareDocument(baseUri As String, nextbaseUri As String) As Integer
        Return 0
    End Function
 
    Public Overrides Function PreserveWhitespace(node As XPathNavigator) As Boolean
        Return True
    End Function
 
    Public Overrides Function ResolveFunction(prefix As String, name As String, ArgTypes() As XPathResultType) As IXsltContextFunction
        Select Case name
            Case "get-property"
                Return New GetPropertyExtensionFunction()
            Case Else
                Return Nothing
        End Select
    End Function
 
    Public Overrides Function ResolveVariable(prefix As String, name As String) As IXsltContextVariable
        Throw New NotImplementedException()
    End Function
End Class


Тестирование



Для тестирования создадим простой HTML-документ.
Кликните здесь для просмотра всего текста
Код html5 Выделить
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Тестовая страница</title>
    <style>
        body {
        background-color:aliceblue;
        }
 
    </style>
</head>
<body>
    <h1 class="qwerty">Привет</h1>
    <p>
        Текст приветствия
        <span class="qwerty">Кверти</span>
    </p>
 
    <div id="div1"></div>
    <script>
        document.getElementById("div1").innerText = "hello";
    </script>
</body>
</html>

Для загрузки документа и получения навигатора будем использовать два вспомогательных метода
Код vbnet Выделить
    Function LoadDocument() As mshtml.HTMLDocument
        Dim doc As New mshtml.HTMLDocument
        doc.open()
        CType(doc, mshtml.IHTMLDocument2).writeln(New Object() {IO.File.ReadAllText(IO.Path.Combine(currentDir, "html/HTMLPage1.html"))})
        doc.close()
        Return doc
    End Function
 
    Function GetNavigator() As XPath.XPathNavigator
        Dim doc = LoadDocument()
        Return New HNavigator(doc.documentElement)
    End Function
Создадим метод, который будет получать данные, возвращаемые XPath-выражением, использующим наш метод get-property.
Код vbnet Выделить
    Function ExtractXPathData(xpath As String) As String
        Dim nav = GetNavigator()
        Dim xpathExpr = nav.Compile(xpath)
        xpathExpr.SetContext(New XContext())
        Return nav.Evaluate(xpathExpr)
    End Function
Попробуем его использовать
Код vbnet Выделить
Console.WriteLine(ExtractXPathData("get-property(//H1, 'outerHTML')"))
На выходе получаем следующее
Код html5 Выделить
<H1 class=qwerty>Привет</H1>
То есть мы, как и ожидалось получили HTML-код первого в документе элемента H1.
Далее попробуем выполнить отбор узлов по условию, использующему нашу функцию.
Код vbnet Выделить
        For Each navigator As XPath.XPathNavigator In SelectNodes("//*[get-property('className') = 'qwerty']")
            Console.WriteLine(navigator.OuterXml)
        Next
Как известно, значение атрибута class возвращает свойство className, поэтому в данном случае из документа должны быть извлечены элемены, имеющие атрибут class со значением querty. Можно было написать //*[@class=’querty’] и получить тот же эффект, но в данном случае целью является демонстрация работы нашей фукнции. Получаем следующий вывод
Код xml Выделить
<H1 class="qwerty">Привет</H1>
<SPAN class="qwerty">Кверти</SPAN>
На что здесь следует обратить внимание. В выражениях имена элементов используются в верхнем регистре, хотя в документе они написаны в нижнем. Я об этом уже упоминал, когда писал о реализации свойста LocalName навигатора. Это поведение можно изменить там же, но лучше этого не делать. Кроме того, несложно заметить, что при запуске первой функции, значение атрибута представлено без кавычек, а вторая выводит значения атрибутов в кавычках. Здесь все дело в том, что в первом случае мы получаем то, что выдает нам библиотека mshtml на запрос свойства outerHTML, а во втором – наш навигатор сам формирует XML код, исходя из известных ему данных об узле.


Использование XSLT



Средства работы с XSLT позволяют работать не только с текстовым представлением документа и объектом XmlDocument, но и с другими XML-объектами, одним из которых является как раз-таки XPathNavigator. А стало быть, реализовав навигатор для HTML-документа, мы автоматически получаем возможность выполнять преобразование документа с помощью этого замечательного языка.
Для начала возьмем тождественное преобразование, при добавлении XSLT-документа в Visual Studio создается именно оно.
Кликните здесь для просмотра всего текста
Код xml Выделить
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:output method="xml" indent="yes"/>
 
    <xsl:template match="@* | node()">
        <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>


Добавим в код функцию, выполняющую это преобразование над нашим документом
Код vbnet Выделить
   Sub TransformWithNavigatorEqual()
        Dim xtrans As New Xml.Xsl.XslCompiledTransform()
        Dim nav = GetNavigator()
        xtrans.Load(IO.Path.Combine(currentDir, "xslt/EqualTransform.xslt"))
        Dim sw = Stopwatch.StartNew
        Using stream = IO.File.Create(IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "transformedpage.xml"))
            xtrans.Transform(nav, Nothing, stream)
        End Using
        sw.Stop()
        Console.WriteLine(sw.Elapsed.TotalSeconds)
    End Sub
Результат будет сохранен на рабочий стол под именем transformedpage.xml, а на консоль буде выведено время выполнения преобразования.
Кликните здесь для просмотра всего текста
Код html5 Выделить
<?xml version="1.0" encoding="utf-8"?>
<HTML lang="en" xmlns="http://www.w3.org/1999/xhtml">
  <HEAD>
    <TITLE>Тестовая страница</TITLE>
    <META charset="utf-8" />
    <STYLE>BODY {
    BACKGROUND-COLOR: aliceblue
}
</STYLE>
  </HEAD>
  <BODY>
    <H1 class="qwerty">Привет</H1>
    <P>Текст приветствия <SPAN class="qwerty">Кверти</SPAN> </P>
    <DIV id="div1">hello</DIV>
    <SCRIPT>
        document.getElementById("div1").innerText = "hello";
    </SCRIPT>
  </BODY>
</HTML>

В результате мы получили корректный XML-документ с именами элементов в верхнем регистре. Кроме того, следует обратить внимание, что изначально пустой элемент div#div1 имеет содержимое, добавленное скриптом, чего не произошло бы, работай мы с каким-нибудь парсером HTML вроде HtmlAgilityPack.
Далее, интересно было бы исследовать еще одни момент, а именно – расширение XSLT. Мы попробуем использовать XSLT со скриптом. Поскольку наш навигатор возвращает имена элементов в верхнем регистре, а в XSLT и в XPath нет встроенной функции переводящей в нижней регистр (хотя, строго говоря, можно для этих целей использовать функцию translate), мы создадим такую функцию в скрипте и используем ее в преобразовании.
Кликните здесь для просмотра всего текста
Код xml Выделить
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
xmlns:x="urn:my-extension-funcs">
  <xsl:output method="xml" indent="yes"/>
 
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@*"/>
      <xsl:apply-templates select="node()"/>
    </xsl:copy>
  </xsl:template>
 
  <xsl:template match="*">
    <xsl:element name="{x:ToLower(name())}">
      <xsl:apply-templates select="@*"/>
      <xsl:apply-templates select="node()"/>
    </xsl:element>
  </xsl:template>
 
  <msxsl:script implements-prefix="x" language="vb">
    <![CDATA[
Public Function ToLower(s As String) As String
  Return s.ToLower()
End Function
  
]]>
 
  </msxsl:script>
</xsl:stylesheet>



Код метода, использующего это преобразование будет таким
Код vbnet Выделить
    Sub TransformWithNavigatorToLower()
        Dim xtrans As New Xml.Xsl.XslCompiledTransform()
 
        Dim nav = GetNavigator()
        xtrans.Load(IO.Path.Combine(currentDir, "xslt/TagNameToLower.xslt"), New Xsl.XsltSettings() With {.EnableScript = True, .EnableDocumentFunction = True}, Nothing)
        Dim sw = Stopwatch.StartNew
        Using stream = IO.File.Create(IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "transformedpage-tolower.xml"))
            xtrans.Transform(nav, Nothing, stream)
        End Using
        sw.Stop()
        Console.WriteLine(sw.Elapsed.TotalSeconds)
    End Sub
А результат – таким
Кликните здесь для просмотра всего текста
Код xml Выделить
<?xml version="1.0" encoding="utf-8"?>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Тестовая страница</title>
    <meta charset="utf-8" />
    <style>BODY {
    BACKGROUND-COLOR: aliceblue
}
</style>
  </head>
  <body>
    <h1 class="qwerty">Привет</h1>
    <p>Текст приветствия <span class="qwerty">Кверти</span> </p>
    <div id="div1">hello</div>
    <script>
        document.getElementById("div1").innerText = "hello";
    </script>
  </body>
</html>


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

Комментариев нет :

Отправить комментарий