Форум программистов, компьютерный форум, киберфорум
diadiavova
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

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

Запись от diadiavova размещена 29.08.2016 в 23:48
Показов 3810 Комментарии 0

  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 должна быть подключена к проекту).

Кликните здесь для просмотра всего текста
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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.

Кликните здесь для просмотра всего текста
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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


Теперь код самого навигатора.
Кликните здесь для просмотра всего текста
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
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, либо один из его атрибутов.
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    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()). Правда это негативно сказывается на производительности.
VB.NET
1
2
3
4
5
6
7
8
9
10
11
    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.
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    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 для таких узлов применяет иную логику, нежели для всех остальных
VB.NET
1
2
3
4
5
6
    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
Кликните здесь для просмотра всего текста
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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.
Кликните здесь для просмотра всего текста
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!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>

Для загрузки документа и получения навигатора будем использовать два вспомогательных метода
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
    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.
VB.NET
1
2
3
4
5
6
    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
Попробуем его использовать
VB.NET
1
Console.WriteLine(ExtractXPathData("get-property(//H1, 'outerHTML')"))
На выходе получаем следующее
HTML5
1
<H1 class=qwerty>Привет</H1>
То есть мы, как и ожидалось получили HTML-код первого в документе элемента H1.
Далее попробуем выполнить отбор узлов по условию, использующему нашу функцию.
VB.NET
1
2
3
        For Each navigator As XPath.XPathNavigator In SelectNodes("//*[get-property('className') = 'qwerty']")
            Console.WriteLine(navigator.OuterXml)
        Next
Как известно, значение атрибута class возвращает свойство className, поэтому в данном случае из документа должны быть извлечены элемены, имеющие атрибут class со значением querty. Можно было написать //*[@class=’querty’] и получить тот же эффект, но в данном случае целью является демонстрация работы нашей фукнции. Получаем следующий вывод
XML
1
2
<H1 class="qwerty">Привет</H1>
<SPAN class="qwerty">Кверти</SPAN>
На что здесь следует обратить внимание. В выражениях имена элементов используются в верхнем регистре, хотя в документе они написаны в нижнем. Я об этом уже упоминал, когда писал о реализации свойста LocalName навигатора. Это поведение можно изменить там же, но лучше этого не делать. Кроме того, несложно заметить, что при запуске первой функции, значение атрибута представлено без кавычек, а вторая выводит значения атрибутов в кавычках. Здесь все дело в том, что в первом случае мы получаем то, что выдает нам библиотека mshtml на запрос свойства outerHTML, а во втором – наш навигатор сам формирует XML код, исходя из известных ему данных об узле.


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

Средства работы с XSLT позволяют работать не только с текстовым представлением документа и объектом XmlDocument, но и с другими XML-объектами, одним из которых является как раз-таки XPathNavigator. А стало быть, реализовав навигатор для HTML-документа, мы автоматически получаем возможность выполнять преобразование документа с помощью этого замечательного языка.
Для начала возьмем тождественное преобразование, при добавлении XSLT-документа в Visual Studio создается именно оно.
Кликните здесь для просмотра всего текста
XML
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>


Добавим в код функцию, выполняющую это преобразование над нашим документом
VB.NET
1
2
3
4
5
6
7
8
9
10
11
   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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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>



Код метода, использующего это преобразование будет таким
VB.NET
1
2
3
4
5
6
7
8
9
10
11
12
    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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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>


Таким образом мы убедились, что все прекрасно работает.
>>
Вложения
Тип файла: zip MshtmlXPathTests.zip (108.1 Кб, 373 просмотров)
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Access
VikBal 11.12.2025
Помогите пожалуйста !! Как объединить 2 одинаковые БД Access с разными данными.
Новый ноутбук
volvo 07.12.2025
Всем привет. По скидке в "черную пятницу" взял себе новый ноутбук Lenovo ThinkBook 16 G7 на Амазоне: Ryzen 5 7533HS 64 Gb DDR5 1Tb NVMe 16" Full HD Display Win11 Pro
Музыка, написанная Искусственным Интеллектом
volvo 05.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 17.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru