2024-10-23 18:40:42
,某些文章具有时效性,若有错误或已失效,请在下方留言。开始解析
首先,我们将创建两个类来保存所有数据:一个名为 XMLNode
的类代表解析 XML
中的一个节点,另一个名为 MicroDOM
的类负责将 XML 实际解析为节点。XML
的本质是整个树最终由一个根节点所拥有,因此当我们的解析器完成解析后,它将返回一个代表根节点的 XMLNode
。
首先,我们定义一个 XML
节点。这需要是一个类,这样我们才能逐步建立节点–请记住,Apple 的系统是基于回调的,因此每个节点都会有多个回调。
class XMLNode {
let tag: String
var data: String
let attributes: [String: String]
var childNodes: [XMLNode]
init(tag: String, data: String, attributes: [String : String], childNodes: [XMLNode]) {
self.tag = tag
self.data = data
self.attributes = attributes
self.childNodes = childNodes
}
}
请注意,每个节点都有自己的子节点数组,这就是我们的树形结构。
稍后我们将为该类添加更多内容,但首先我们需要为 XML 解析器本身添加第二个类。这也需要一个类,但原因不同:它将作为苹果公司自己的 XML 解析器的委托,因为它来自 Objective-C,所以我们的委托需要是一个继承于 NSObject 的类。
我们将赋予它三种属性:
XMLParser
的实例,它是 Foundation 框架的 XML 解析类。它将报告找到的每个元素的开始、结束和文本。XML
节点堆栈,存储我们在解析树中的当前位置。将其存储为堆栈意味着当一个元素结束时,我们可以将其从堆栈中取出,并返回给它的父节点。- 树中的最高节点。
我们还需要提供一个初始化器来设置 XMLParser
,以便正确报告解析事件,并提供一个 parse()
方法来运行整个解析系统,检查错误,然后发回树的顶层。
你可能会认为将初始化方法和解析方法分开是不必要的,但这样做可以让我们保持内部实现的私有性–就该类的用户而言,他们不能读写任何属性,只能调用一些方法。
class MicroDOM: NSObject, XMLParserDelegate {
private let parser: XMLParser
private var stack = [XMLNode]()
private var tree: XMLNode?
init(data: Data) {
parser = XMLParser(data: data)
super.init()
parser.delegate = self
}
func parse() -> XMLNode? {
parser.parse()
guard parser.parserError == nil else {
return nil
}
return tree
}
}
现在我们可以编写一些代码来解析 XML 示例:
let string = "<root><h1>Hello!</h1><h1>World!</h1></root>"
let dom = MicroDOM(data: Data(string.utf8))
let tree = dom.parse()
print(tree?.tag ?? "")
这段代码应该可以编译,但它实际上还不会做任何事情–我们已经告诉 XMLParser
让它工作,但实际上我们并没有读取它找到的内容。
构建树节点
为了让我们的解析工作真正达到预期效果,我们需要在 MicroDOM
中添加三个将被 XMLParser
调用的方法:一个是新元素开始时的方法,一个是元素结束时的方法,还有一个是找到文本内容时的方法。
这时,我们的节点堆栈就派上用场了,因为当一个新元素开始出现时,我们可以创建一个 XMLNode
实例,并将其作为之前元素的子元素推送到堆栈中。然后,当元素结束时,我们可以将其从堆栈中弹出,并返回到前一个元素。
这三个方法都有非常精确的名称。这是因为我们自己并没有调用它们,而是 XMLParser
在发生任何事情时都会调用它们–我们的类充当了解析委托的角色,请记住。
首先,我们将添加一个 didStartElement
方法,当 XMLParser
开始一个新的 XML
元素时,它将调用该方法。这可能是第一个元素,也可能是其他元素的子元素–我们并不关心这些,因为无论如何,我们只需从其数据中创建一个 XMLNode
,并将其添加到我们的节点栈中。
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
let node = XMLNode(tag: elementName, data: "", attributes: attributeDict, childNodes: [])
stack.append(node)
}
接下来,我们需要编写一个 didEndElement
,当当前元素结束时,它会被调用。这需要更多的工作,因为这是我们制作树形结构的地方:
- 我们先从堆栈中删除最后一个元素,也就是最近创建的元素。
- 如果堆栈的最后一项仍有可读取的元素,那么就将新元素添加到最后一项,作为其子元素之一。
- 否则,如果堆栈为空,那么新元素就是树的根,因此将其赋值为 tree。
代码如下:
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
let lastElement = stack.removeLast()
if let last = stack.last {
last.childNodes += [lastElement]
} else {
tree = lastElement
}
}
最后,我们需要一个 foundCharacters
方法,用于报告在当前元素中发现的任何文本字符串:
func parser(_ parser: XMLParser, foundCharacters string: String) {
stack.last?.data = string
}
有了这些方法,我们的解析工作就可以正常进行了–如果运行我们的测试代码,就会打印出 “root”,这是位于树顶的项的元素名称。
查询结果
虽然 XML
解析工作已经全部完成,但我还想添加至少两个方法,使我们的项目更加生动:一个是读取节点的特定属性,另一个是查找带有特定标记的所有节点。
读取特定属性很简单,因为 XMLParser
已经为我们提供了一个包含所有属性的字典,所以只需返回在寻找的任何关键字即可。将此方法添加到 XMLNode
:
func getAttribute(_ name: String) -> String? {
attributes[name]
}
查找具有特定名称的所有元素需要花费更多的心思,我们需要编写一个递归函数:我们要找出与要查找的名称相匹配的所有子节点,然后调用我们的函数来搜索它们的子节点,以此类推。
最终,这个函数将调用所有子女、孙子女、曾孙子女等的函数,从而得到一个具有特定名称的元素数组,并将其发送回去。
现在将此最终方法添加到 XMLNode
中:
func getElemetsByTagName(_ name: String) -> [XMLNode] {
var results = [XMLNode]()
for node in childNodes {
if node.tag == name {
results.append(node)
}
results += node.getElemetsByTagName(name)
}
return results
}
有了这些,我们现在就可以找到像这样的特定元素:
[vip]
if let tags = tree?.getElemetsByTagName(“h1”) {
for tag in tags {
print(tag.data)
}
}
[/vip]
[erphpdown]
第三方解析库
[/erphpdown]
暂无评论内容