- О языке XPath
- Преимущества XPathNavigator
- Создаем собственный XPathNavigator
- Расширение языка собственной функцией
- Тестирование
- Использование 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. Причины такого выбора достаточно просты:
- HTML очень похож на XML, поэтому работа нашего навигатора будет наглядной.
- В реализации есть некоторые сложности, стало быть преодолев их, можно лучше понять, как работать с навигатором.
- Такой навигатор имеет практическую пользу, поскольку его можно будет использовать и с 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> |
|
Таким образом мы убедились, что все прекрасно работает.
>>
|