轻松解析 XML

温馨提示:本文最后更新于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 的类。

我们将赋予它三种属性:

  1. XMLParser 的实例,它是 Foundation 框架的 XML 解析类。它将报告找到的每个元素的开始、结束和文本。
  2. XML 节点堆栈,存储我们在解析树中的当前位置。将其存储为堆栈意味着当一个元素结束时,我们可以将其从堆栈中取出,并返回给它的父节点。
  3. 树中的最高节点。

我们还需要提供一个初始化器来设置 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,当当前元素结束时,它会被调用。这需要更多的工作,因为这是我们制作树形结构的地方:

  1. 我们先从堆栈中删除最后一个元素,也就是最近创建的元素。
  2. 如果堆栈的最后一项仍有可读取的元素,那么就将新元素添加到最后一项,作为其子元素之一。
  3. 否则,如果堆栈为空,那么新元素就是树的根,因此将其赋值为 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]

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容