MiniDOM: 最简 XML DOM for Swift
简介
MiniDOM 是 Document Object Model 接口的简化实现。它的目的是比完整的 DOM 更简单,但功能足够强大,足以在大多数应用程序中使用。
MiniDOM 已完全文档化和单元测试。它可用于 iOS、macOS、watchOS 和 tvOS。库在 MIT 许可下发布。
要解析 XML 文档,只需创建一个 Parser
对象并调用 parse()
import Foundation
import MiniDOM
func parseXML(url: URL) -> Document? {
let parser = Parser(contentsOf: url)
let result = parser?.parse()
return result?.document
}
结果结构是一个实现 Node
协议的对象树:Document
、Element
、Text
、ProcessingInstruction
、Comment
和 CDATASection
。提供了类似于 DOM 规范中的访问器方法和属性。DOM 树可以使用搜索方法、路径评估方法或使用访问者设计模式进行遍历。以下将详细讨论这些方法。
安装
MiniDOM 支持 CocoaPods、Carthage 和 Swift Package Manager 的安装。
Cocoapods
请将以下内容添加到您的 Podfile
pod 'MiniDOM'
Carthage
将以下内容添加到您的 Cartfile
github "MiniDOM/MiniDOM"
Swift Package Manager
在你的 Package.swift
文件中添加以下依赖项
.package(url: "https://github.com/MiniDOM/MiniDOM/", from: "1.0.0")
依赖
MiniDOM 没有第三方依赖。它只使用 Foundation
类,包括 XMLParser
。单元测试使用 XCUnit
编写。
路径评估
MiniDOM 提供了一种通过路径遍历文档的机制。调用 Document.evaluate(path:)
,传递一个表示元素节点名称的字符串数组 (Node.nodeName
)。例如,考虑以下文档
<a id="1">
<b id="2">
<z id="3"/>
</b>
<c id="4">
<z id="5"/>
</c>
<b id="6"/>
<z id="7"/>
<b id="8">
<z id="9"/>
</b>
</a>
评估路径 ["a", "b", "z"]
(通过调用 document.evaluate(path: ["a", "b", "z"])
)将返回一个包含两个 Element
对象的数组,分别表示 ID 为 3
和 9
的 <z>
元素。
访问者设计模式
在整个MiniDOM库中,使用访问者设计模式来实现涉及遍历DOM树的算法。它提供了一种方便的机制,将算法与其操作的对象结构分开。它允许向DOM结构添加操作而无需修改结构本身。
提供了一个用于启动遍历的Visitor
对象,它是通过调用Node.accept(_:)
实现的。在调用其子节点的Node.accept(_:)
之前,Node
对象会调用适当的Visitor
对象方法,从而执行递归遍历。
Visitor
协议定义了与DOM中的每种Node
类型对应的方法。实现Visitor
协议的类型不需要处理实际的遍历;其方法是由DOM类提供的遍历算法调用的。
关于访问者的简单示例,请参阅Search.swift
中的ElementSearch
类。关于访问者的更复杂示例,请参阅Formatter.swift
中的PrettyPrinter
类。
示例
以下内容来自项目根目录下的MiniDOM.playground
。您可以随时打开它并自行实验。
解析文档
在沙盒的资源部分有一个XML文档。它包含EFF更新RSS源的一个快照。我们将从解析文档开始。
let url = Bundle.main.url(forResource: "eff-updates", withExtension: "rss")!
let parser = Parser(contentsOf: url)
let document = parser?.parse().document
遍历文档
文档的结构大致如下
<rss>
<channel>
<title>...</title>
<link>...</link>
<description>...</description>
<item>
<title>...</title>
<link>...</link>
<description>...</description>
</item>
<item>...</item>
...
</channel>
</rss>
让我们首先获取文档元素或根节点。
let rss = document?.documentElement
rss?.nodeName
结果
"rss"
<rss>
元素应该有一个子元素:一个<channel>
元素。
let channel = rss?.firstChildElement
channel?.nodeName
结果
"channel"
<channel>
元素应该有50个<item>
子元素。
let items = channel?.childElements(withName: "item")
items?.count
结果
50
<item>
元素应该有一个子元素:<title>
。
let itemTitles = items?.flatMap { itemElement -> String? in
let titleElement = itemElement.childElements(withName: "title").first
return titleElement?.textValue
}
itemTitles
结果
0 "Stupid Patent of the Month: Storing Files in Folders"
1 "NAFTA Renegotiation Will Resurrect Failed TPP Proposals"
2 "New Report Aims to Help Criminal Defense Attorneys Challenge Secretive Government Hacking"
3 "The Most Powerful Single Click in Your Facebook Privacy Settings"
4 "Repealing Broadband Privacy Rules, Congress Sides with the Cable and Telephone Industry"
...
还有<link>
元素,它是<channel>
元素的子元素,也是每个<item>
元素的子元素。我们可以找到所有的这些元素。
let linkElementsFromDocument = document?.elements(withTagName: "link")
let linkURLsFromDocument = linkElementsFromDocument?.flatMap { $0.textValue }
linkURLsFromDocument
结果
0 "https://www.eff.org/rss/updates.xml"
1 "https://www.eff.org/deeplinks/2017/03/stupid-patent-month-storing-files-folders"
2 "https://www.eff.org/deeplinks/2017/03/nafta-renegotiation-will-resurrect-failed-tpp-proposals"
3 "https://www.eff.org/deeplinks/2017/03/eff-says-no-so-called-moral-rights-copyright-expansion"
4 "https://www.eff.org/deeplinks/2017/03/new-report-aims-help-criminal-defense-attorneys-challenge-secretive-government"
5 "https://www.eff.org/deeplinks/2017/03/most-powerful-single-click-your-facebook-privacy-settings"
...
路径评估
<channel>
元素的子元素<item>
每个都应该有一个<link>
子元素。通过使用路径表达式,我们可以收集在<channel>
元素下的所有<link>
元素的文本子元素。
let linkTextNodesViaPath = document?.evaluate(path: ["rss", "channel", "item", "link", "#text"])
let linkURLsViaPath = linkTextNodesViaPath?.flatMap { $0.nodeValue }
linkURLsViaPath
结果
0 "https://www.eff.org/deeplinks/2017/03/stupid-patent-month-storing-files-folders"
1 "https://www.eff.org/deeplinks/2017/03/nafta-renegotiation-will-resurrect-failed-tpp-proposals"
2 "https://www.eff.org/deeplinks/2017/03/eff-says-no-so-called-moral-rights-copyright-expansion"
3 "https://www.eff.org/deeplinks/2017/03/new-report-aims-help-criminal-defense-attorneys-challenge-secretive-government"
4 "https://www.eff.org/deeplinks/2017/03/most-powerful-single-click-your-facebook-privacy-settings"
...
访客
我们可以通过访客收集文档中的所有<title>
元素。
class TitleCollector: Visitor {
var titles: [String] = []
public func beginVisit(_ element: Element) {
if element.tagName == "title", let title = element.textValue {
titles.append(title)
}
}
}
let titleCollector = TitleCollector()
document?.accept(titleCollector)
titleCollector.titles
结果
0 "Deeplinks"
1 "Stupid Patent of the Month: Storing Files in Folders"
2 "NAFTA Renegotiation Will Resurrect Failed TPP Proposals"
3 "New Report Aims to Help Criminal Defense Attorneys Challenge Secretive Government Hacking"
4 "The Most Powerful Single Click in Your Facebook Privacy Settings"
5 "Repealing Broadband Privacy Rules, Congress Sides with the Cable and Telephone Industry"
问题与贡献
请报告您找到的问题。
欢迎提交拉取请求。请确保任何添加文档并进行了单元测试。我们致力于保持100%的文档和测试覆盖率。