Swift 的 XMLTools

Swift 5.1 license

XMLTools 是一组用于解析、评估、操作和序列化复杂 XML 结构的 API。它完全使用 Swift 编程语言编写,旨在在所有支持 Swift 的平台(例如 macOS、iOS)上运行。

XMLTOOLS 提供以下功能

动机

由于 Apple 仅在其所有平台(macOS 除外,macOS 具有高级 XML API)上提供低级 XMLParser,因此有很多开源项目提供此类 API,最值得注意的是 SWXMLHashSwiftyXMLParser

我在 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

集成

Swift 包管理器

待办事项

类似 XPath 的选择 API

以下示例 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>

将字符串解析为 Infoset

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
}

Swift XMLTools API 的 XPath 等效项

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")

序列化 XML

// 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")
}

从头开始创建 XML

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

XMLTools 使用 Swift 包管理器

cd SwiftXMLTools
swift package generate-xcodeproj
swift build
swift test

创建发布版本

git tag <VERSION>
git push origin <VERSION>