- О языке 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 должна быть подключена к проекту).
Кликните здесь для просмотра всего текста
Код 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
Код 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
Код 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
Код 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
Код 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>
Далее попробуем выполнить отбор узлов по условию, использующему нашу функцию.
Код vbnet | Выделить |
For Each navigator As XPath.XPathNavigator In SelectNodes("//*[get-property('className') = 'qwerty']")
Console.WriteLine(navigator.OuterXml)
Next
Код xml | Выделить |
<H1 class="qwerty">Привет</H1>
<SPAN class="qwerty">Кверти</SPAN>
Использование 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
Кликните здесь для просмотра всего текста
Код 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>
Таким образом мы убедились, что все прекрасно работает.
Комментариев нет :
Отправить комментарий