XMLTools
是一组用于解析、评估、操作和序列化复杂 XML 结构的 API。它完全使用 Swift 编程语言编写,旨在在所有支持 Swift 的平台(例如 macOS、iOS)上运行。
XMLTOOLS
提供以下功能
由于 Apple 仅在其所有平台(macOS 除外,macOS 具有高级 XML API)上提供低级 XMLParser,因此有很多开源项目提供此类 API,最值得注意的是 SWXMLHash 和 SwiftyXMLParser。
我在 GitHUB 上找到的所有项目的问题在于,它们仅支持最简单的 XML 结构和查询。它们中的大多数从 SwiftyJSON 中获得灵感,并将 XML 作为 JSON 处理。这种方法存在两个问题:1) 大多数遗留 XML 系统使用相当复杂的 XML 结构,并大量使用命名空间;2) 如果有人创建新的简单协议,他们无论如何都会使用 JSON。
XMLTools
试图弥合这一差距,并使用 Swift 编程语言的现代功能提供“老式 XML”。
let parser = XMLTools.Parser()
let xml: XMLTools.Infoset
do {
xml = try parser.parse(contentsOf: "https://ec.europa.eu/information_society/policy/esignature/trusted-list/tl-mp.xml")
} catch {
print (error)
return
}
xml.namespaceContext.declare(withNoPrefix: "http://uri.etsi.org/02231/v2#")
print(xml["TrustServiceStatusList", "SchemeInformation", "TSLType"].text)
// prints http://uri.etsi.org/TrstSvc/TrustedList/TSLType/EUlistofthelists
待办事项
以下示例 XML 基于 w3schools.com XPath 教程
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book>
<title lang="en">Harry Potter: The Philosopher's Stone</title>
<price>24.99</price>
<pages>223</pages>
</book>
<book>
<title lang="en">Harry Potter: The Chamber of Secrets</title>
<price>29.99</price>
<pages>251</pages>
</book>
<book>
<title lang="en">Learning XML</title>
<price>39.95</price>
<pages>432</pages>
</book>
<book>
<title lang="de">IT-Sicherheit: Konzepte - Verfahren - Protokolle</title>
<price>69.95</price>
<pages>932</pages>
</book>
</bookstore>
let xmlString = """
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book>
<title lang="en">Harry Potter: The Philosopher's Stone</title>
<price>24.99</price>
<pages>223</pages>
</book>
<book>
<title lang="en">Harry Potter: The Chamber of Secrets</title>
<price>29.99</price>
<pages>251</pages>
</book>
<book>
<title lang="en">Learning XML</title>
<price>39.95</price>
<pages>432</pages>
</book>
<book>
<title lang="de">IT-Sicherheit: Konzepte - Verfahren - Protokolle</title>
<price>69.95</price>
<pages>932</pages>
</book>
</bookstore>
"""
let parser = XMLTools.Parser()
let xml: XMLTools.Infoset
do {
xml = try parser.parse(string: xmlString, using: .utf8)
} catch {
print (error)
return
}
Xpath | Swift |
---|---|
bookstore |
xml["bookstore"] xml.select("bookstore") |
/bookstore |
xml.selectDocument()["bookstore"] xml.selectDocument().select("bookstore") |
bookstore/book |
xml["bookstore", "book"] xml["bookstore"]["book"] xml.select("bookstore", "book") xml.select("bookstore").select("book") |
//book |
xml.descendants("book") |
//@lang |
xml.descendants().attr("lang") |
/bookstore/book[1] |
xml["bookstore", "book", 0] xml["bookstore", "book"].item(0) 请注意 Swift 中基于 0 的索引 |
/bookstore/book[last()] |
xml["bookstore", "book"].last() |
/bookstore/book[position()<3] |
xml["bookstore", "book"].select(byPosition: { $0 < 2 }) |
//title[@lang] |
xml.descendants("title").select({ $0.attr("lang").text != "" }) |
//title[@lang='en'] |
xml.descendants("title").select({ $0.attr("lang").text == "en" }) |
/bookstore/book[pages>300] |
xml["bookstore", "book"].select({ $0["pages"].number > 300 }) |
/bookstore/book[price>35.00] |
xml["bookstore", "book"].select({ $0["price"].number > 35 }) |
/bookstore/book[price>40.00]/title |
xml["bookstore", "book"].select({ $0["price"].number > 40 }).select("title") |
* |
xml.select() |
/bookstore/book/title/@* |
xml["bookstore", "book", "title"].attr() |
/bookstore/book/title[0]/node() |
xml["bookstore", "book", "title", 0].selectNode() |
/bookstore/* |
xml["bookstore"].select() |
//* |
xml.descendants() |
count(//book) |
xml.descendants("book").count |
bookstore/book[starts-with(title,'Harry Potter')] |
xml["bookstore", "book"].select({ $0["title"].text.starts(with: "Harry Potter") }) |
考虑来自 关于 WSDL 的 Wikipedia 文章 的示例
let wsdlSourceXML =
"""
<?xml version="1.0" encoding="UTF-8"?>
<description xmlns="http://www.w3.org/ns/wsdl"
xmlns:tns="http://www.tmsws.com/wsdl20sample"
xmlns:whttp="http://schemas.xmlsoap.org/wsdl/http/"
xmlns:wsoap="http://schemas.xmlsoap.org/wsdl/soap/"
targetNamespace="http://www.tmsws.com/wsdl20sample">
<documentation>
This is a sample WSDL 2.0 document.
</documentation>
<!-- Abstract type -->
<types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="http://www.tmsws.com/wsdl20sample"
targetNamespace="http://www.example.com/wsdl20sample">
<xs:element name="request"> ... </xs:element>
<xs:element name="response"> ... </xs:element>
</xs:schema>
</types>
<!-- Abstract interfaces -->
<interface name="Interface1">
<fault name="Error1" element="tns:response"/>
<operation name="Get" pattern="http://www.w3.org/ns/wsdl/in-out">
<input messageLabel="In" element="tns:request"/>
<output messageLabel="Out" element="tns:response"/>
</operation>
</interface>
<!-- Concrete Binding Over HTTP -->
<binding name="HttpBinding" interface="tns:Interface1"
type="http://www.w3.org/ns/wsdl/http">
<operation ref="tns:Get" whttp:method="GET"/>
</binding>
<!-- Concrete Binding with SOAP-->
<binding name="SoapBinding" interface="tns:Interface1"
type="http://www.w3.org/ns/wsdl/soap"
wsoap:protocol="http://www.w3.org/2003/05/soap/bindings/HTTP/"
wsoap:mepDefault="http://www.w3.org/2003/05/soap/mep/request-response">
<operation ref="tns:Get" />
</binding>
<!-- Web Service offering endpoints for both bindings-->
<service name="Service1" interface="tns:Interface1">
<endpoint name="HttpEndpoint"
binding="tns:HttpBinding"
address="http://www.example.com/rest/"/>
<endpoint name="SoapEndpoint"
binding="tns:SoapBinding"
address="http://www.example.com/soap/"/>
</service>
</description>
"""
let parser = XMLTools.Parser()
let xml: XMLTools.Infoset
do {
xml = try parser.parse(string: wsdlSourceXML)
} catch {
print (error)
XCTFail("\(error)")
return
}
由于我们在创建“XMLTools.Parser”时没有指定任何选项,因此当前 Infoset 中没有命名空间声明,并且必须使用限定名称访问每个元素
print (xml[QName("description", uri: "http://www.w3.org/ns/wsdl"), QName("documentation", uri: "http://www.w3.org/ns/wsdl")].text)
即使我们使其更短,仍然不容易阅读
let wsdlURI = "http://www.w3.org/ns/wsdl"
print (xml[QName("description", uri: wsdlURI), QName("documentation", uri: wsdlURI)].text)
更好的方法是声明命名空间。请注意,即使源 XML 没有定义前缀,我们仍然应该使用此处定义的前缀访问元素和属性。这样,代码独立于源命名空间前缀,尤其是在源是生成的并使用像 ns0
这样的神秘前缀时
// equivalent to xmlns:wsdl="http://www.w3.org/ns/wsdl"
xml.namespaceContext.declare("wsdl", uri: "http://www.w3.org/ns/wsdl")
print (xml["wsdl:description", "wsdl:documentation"].text)
如果我们想在不使用前缀的情况下访问 WSDL 元素,我们可以这样做
// equivalent to xmlns="http://www.w3.org/ns/wsdl"
xml.namespaceContext.declare(withNoPrefix: "http://www.w3.org/ns/wsdl")
print (xml["description", "documentation"].text)
这是一个更复杂的示例,演示了 XMLTools
API 的可扩展性
// somewhere on file level
extension NamespaceDeclaration {
public static let Wsdl = NamespaceDeclaration("wsdl", uri: "http://www.w3.org/ns/wsdl")
public static let WsdlSoap = NamespaceDeclaration("wsoap", uri: "http://schemas.xmlsoap.org/wsdl/soap/")
public static let WsdlHttp = NamespaceDeclaration("whttp", uri: "http://schemas.xmlsoap.org/wsdl/http/")
}
// declare the namespaces we want to use
xml.namespaceContext.declare(.Wsdl).declare(.WsdlSoap).declare(.WsdlHttp)
let httpBinding = xml.descendants("wsdl:binding").select {
$0.attr("name").text == "HttpBinding"
}
print (httpBinding["wsdl:operation"].attr("whttp:method").text) // "GET"
let soapBinding = xml.descendants("wsdl:binding").select {
$0.attr("name").text == "SoapBinding"
}
print (soapBinding.attr("wsoap:protocol").text) // "http://www.w3.org/2003/05/soap/bindings/HTTP/"
最后,我们可以偷懒,告诉解析器完全按照 XML 源中显示的方式保留所有命名空间声明
let anotherParser = XMLTools.Parser()
// tell the parser to preserve all namespace prefix declarations
anotherParser.options.preserveSourceNamespaceContexts = true
let anotherXML: XMLTools.Infoset
do {
anotherXML = try anotherParser.parse(string: wsdlSourceXML)
} catch {
print (error)
XCTFail("\(error)")
return
}
print (anotherXML["description"].name().namespaceURI) // "http://www.w3.org/ns/wsdl"
XCTAssertEqual(anotherXML["description"].name().namespaceURI, "http://www.w3.org/ns/wsdl")
// Parse XML
let xmlLocation = "https://raw.githubusercontent.com/spilikin/SwiftXMLTools/master/Testfiles/xmldsig-core-schema.xsd"
let parser = XMLTools.Parser()
// tell the parser to preserve the namespace declarations (prefixes)
parser.options.preserveSourceNamespaceContexts = true
let xml: XMLTools.Infoset
do {
xml = try parser.parse(contentsOf: xmlLocation)
} catch {
print("\(error)")
return
}
if let indentedData = xml.document().data(.indent) {
print (String(data: indentedData, encoding:.utf8)! )
} else {
print ("Cannot convert XML to Data")
}
struct Book {
let title: String
let lang: String
let price: Decimal
let pages: Int
}
let bookstore = [
Book(title: "Harry Potter: The Philosopher's Stone", lang: "en", price: 24.99, pages: 223),
Book(title: "Harry Potter: The Chamber of Secrets", lang: "en", price: 29.99, pages: 251),
Book(title: "Learning XML", lang: "en", price: 39.95, pages: 432),
Book(title: "IT-Sicherheit: Konzepte - Verfahren - Protokolle", lang: "de", price: 69.95, pages: 932),
]
let builtXML = Document().select()
builtXML.appendElement("bookstore")
for book in bookstore {
builtXML["bookstore"].appendElement("book")
.appendElement("title")
.manipulate{ $0.text = book.title; $0.attr("lang", setValue: book.lang) }
.parent()
.appendElement("price").manipulate{ $0.number = book.price}.parent()
.appendElement("pages").manipulate{ $0.number = book.pages }
}
let xmlData = builtXML.document().data(.indent,.omitXMLDeclaration)
print ( String(data: xmlData!, encoding: .utf8)! )
应产生以下输出
<bookstore>
<book>
<title lang="en">Harry Potter: The Philosopher's Stone</title>
<price>24.99</price>
<pages>223</pages>
</book>
<book>
<title lang="en">Harry Potter: The Chamber of Secrets</title>
<price>29.99</price>
<pages>251</pages>
</book>
<book>
<title lang="en">Learning XML</title>
<price>39.95</price>
<pages>432</pages>
</book>
<book>
<title lang="de">IT-Sicherheit: Konzepte - Verfahren - Protokolle</title>
<price>69.95</price>
<pages>932</pages>
</book>
</bookstore>
XMLTools
使用 Swift 包管理器
cd SwiftXMLTools
swift package generate-xcodeproj
swift build
swift test
创建发布版本
git tag <VERSION>
git push origin <VERSION>