一个用 Swift 编写的用于处理 XML 的库。
本库基于 Apache License v2.0 许可发布,并附带运行时库例外条款。
let transformation = XTransformation {
XRule(forElements: "table") { table in
table.insertNext {
XElement("caption") {
"Table: "
table.children({ $0.name.contains("title") }).content
}
}
}
XRule(forElements: "tbody", "tfoot") { tablePart in
for cell in tablePart.children("tr").children("th") {
cell.name = "td"
}
}
XRule(forRegisteredAttributes: "label") { label in
label.element["label"] = label.value + ")"
}
}
注意
尽管版本号很高,但本库尚未处于“最终”状态,即可能仍然存在错误,或者会进行一些重大改进,并且可能会发生破坏性更改,而主版本号不会增加。此外,代码中将添加更多注释。而且,当达到最终状态时,该库可能会使用新的存储库 URL 进一步开发(并且版本号将设置回较低的版本)。更多通知将在此处添加。请访问那里获取联系信息。
我们计划在 2025 年初发布最终版本。(届时,该库将已在生产环境中使用。)对于所有已经对该库感兴趣的人,感谢您的耐心等待!
更新 1(2023 年 5 月): 我们最近对 API 做了一些小的更改(不再有公开的 XSpot
,但您可以为 XText
设置 isolated
),并修复了一些问题,目前正在努力为此库和 SwiftXMLParser
添加更多测试。
更新 2(2023 年 7 月): 为了保持 XML 树的小巧,我们移除了直接访问文档中具有特定名称的属性的功能, 因此也移除了为属性制定规则的功能(属性规则在应用程序中很少使用)。您将需要检查文档的后代(如果在解析期间没有捕获相应的事件),而不是直接访问具有特定名称的属性,也许可以保存结果。当我们添加验证工具时,将提供更简单的替代丢失功能的方案: 当使用适当的模式时,您将能够查找哪些元素——根据模式——可以设置某个属性,然后您可以直接访问这些元素。
更新 3(2023 年 7 月): 将 havingProperties
重命名为 conformingTo
。
更新 4(2023 年 7 月): 命名空间处理现在处于最终状态,请参阅关于 XML 输入限制的新章节和关于如何处理 XML 命名空间的已更改章节。
更新 5(2023 年 7 月): 为了进一步简化库,移除了跟踪更改(属性的更改)的功能。在大多数情况下,当您必须跟踪更改时,您需要一种更好的设置这些属性的方法,因此在每次设置属性时都会有负担,但用处不大。
更新 6(2023 年 8 月): 将 conformingTo
重命名为 when
。
更新 7(2023 年 8 月): 为了符合 Swift 5.9 中的一些类型检查,我们必须要求 Apple 平台为 macOS 13、iOS 16、tvOS 16 或 watchOS 9。
更新 8(2023 年 8 月): 将 applying
重命名为 with
。
更新 9(2023 年 9 月): 再次将 with
重命名为 applying
。将 when
重命名为 fullfilling
。将 hasProperties
重命名为 fullfills
。它们对于单个项目的实现现在通过协议完成。
更新 10(2023 年 10 月): 使用 element(_:)
而不是 element(ofName:)
,以更好地匹配其他接受名称的方法。
更新 11(2023 年 10 月): 现在使用 XProductionTemplate
和 XActiveProduction
而不是 XProduction
,请参阅下面更新的描述。
更新 11(2023 年 10 月): 删除 XProductionTemplate
和 XActiveProduction
实现的 “X” 前缀。
更新 12(2023 年 10 月): XNode.write(toFile:)
重命名为 XNode.write(toPath:)
,XNode.write(toFileHandle:)
重命名为 XNode.write(toFile:)
。
更新 13(2023 年 12 月): texts
重命名为 immediateTexts
,以免与 allTexts
混淆,text
重命名为 allTextsCollected
。添加了 immediateTextsCollected
和 allTextsReversed
变体。
更新 14(2023 年 12 月): 具有整数值的 XContent、XElement 或 XText 序列的下标表示法现在从 1 开始计数。
更新 15(2023 年 12 月): 移除 immediateTextsCollected
。
更新 16(2023 年 12 月): 方法 child(...)
重命名为 firstChild(...)
。
更新 17(2023 年 12 月): 为复杂转换添加了一些跟踪功能。
更新 18(2024 年 1 月): XContentLike
重命名为 XContentConvertible
。当使用 SwiftXML 时,新类型可以符合 XContentConvertible
,因此可以作为 XML 插入。asContent
属性不再必要并已移除,... as XContentConvertible
(以前是 ... as XContentLike
)也应该不再必要。
更新 19(2024 年 3 月): description
为 XText
添加引号。
更新 20(2024 年 5 月): 将 allTextsCollected
重命名为 allTextsCombined
。
更新 21(2024 年 9 月): 创建文档时,您可以指定要(再次)注册的属性名称。移除 extension FileHandle: TextOutputStream
。
更新 22(2024 年 10 月): 更多 ...IncludingSelf
版本用于迭代,例如 nextElementsIncludingSelf
。
更新 22(2024 年 10 月): 默认情况下,内部实体解析器现在必须解析呈现给它的所有实体。
更新 23(2024 年 10 月): 添加了 ...Close...
版本,例如 nextCloseElements
。
更新 24(2024 年 11 月): 使用 hasNext
和 hasPrevious
而不是 hasNextTouching
和 hasPreviousTouching
。
更新 25(2024 年 11 月): 使用属性 clone
和 shallowCLone
而不是方法 clone()
和 shallowCLone()
。
更新 26(2024 年 11 月): HTML 生成:将 suppressPrettyPrintBeforeLeadingAnchor
重命名为 suppressUncessaryPrettyPrintAtAnchors
。
更新 27(2024 年 11 月): 将 backLink
重命名为 backlink
,finalBackLink
重命名为 finalBacklink
。新方法 setting(backlink:)
和 copyingBacklink(from:)
。
更新 28(2024 年 12 月): 新增:immediateTextsCombined
。
更新 29(2025 年 1 月): 新增:backlinkOrSelf
。
For-in 循环不适用于可选类型,例如 Swift 中的可选链。但是,在使用此 XML 库时,能够这样做有时可能会很方便。为了能够循环遍历可选类型,请包含来自 https://github.com/stefanspringer1/LoopsOnOptionals 的非常小的 LoopsOnOptionals
软件包。
当具有以下 XDocument
的扩展时
extension XDocument {
var metaDataSection: XElement? { ... }
}
那么使用 LoopsOnOptionals
软件包,您可以编写
for metaDataItem in myDocument.metaDataSection?.children("item") {
...
}
当然,尤其是在这种简单的情况下,您可以表达相同的含义,而无需使用 LoopsOnOptionals
软件包
if let metaDataSection = myDocument.metaDataSection {
for metaDataItem in metaDataSection.children("item") {
...
}
}
但即使在更复杂的情况下,引入这样的 if let
(或 case let
)表达式也会使代码更难理解。
当在 SwiftWorkflow 框架的上下文中使用 SwiftXML 时,您可以包含 WorkflowUtilitiesForSwiftXML。
该库从源读取 XML 到 XML 文档实例中,并提供转换(或操作)文档的方法,以及将文档写入文件的方法。
该库应该是高效的,并且使用它的应用程序应该是非常易于理解的。
xmlns:...
属性声明命名空间前缀应仅在根元素上。与其他一些 XML 库不同,对内存中构建的文档的操作是“就地”进行的,即不会构建新的 XML 文档。目标是能够有效地对 XML 文档应用许多隔离的操作。但是,始终可以轻松克隆文档,并带有来自旧版本的引用或指向旧版本的引用。
以下功能很重要
以下代码获取任何 <item>
,其整数值 multiply
大于 1,并额外插入一个 multiply
数量少 1 的项目,同时删除现有项目上的 multiply
值(该库将在后续章节中更详细地解释)
let document = try parseXML(fromText: """
<a><item multiply="3"/></a>
""")
for item in document.elements("item") { in
if let multiply = item["multiply"], let n = Int(multiply), n > 1 {
item.insertPrevious {
XElement("item", ["multiply": n > 2 ? String(n-1) : nil])
}
item["multiply"] = nil
}
}
document.echo()
输出为
<a><item/><item/><item/></a>
请注意,在本示例中——只是为了向您展示它可以工作——每个新项目都在当前节点之前插入,但随后仍会被处理。
甚至可以删除迭代返回的元素,而不会停止(惰性!)迭代
let document = try parseXML(fromText: """
<a><item id="1" remove="true"/><item id="2"/><item id="3" remove="true"/><item id="4"/></a>
""")
document.traverse { content in
if let element = content as? XElement, element["remove"] == "true" {
element.remove()
}
}
document.echo()
输出为
<a><item id="2"/><item id="4"/></a>
当然,由于这些迭代是常规序列,因此可以使用所有相应的 Swift 库函数,如 map
和 filter
。但在许多情况下,最好使用内容迭代器上的条件(请参阅关于使用过滤器查找相关内容的章节)或内容迭代器的链接(请参阅关于链接迭代器的章节)。
库的用户还可以提供要应用的一组规则(请参阅开头的代码和关于规则章节中的完整示例)。在这样的规则中,用户定义如何处理具有特定名称的元素或属性。然后可以将一组规则应用于文档,即规则按照其定义的顺序应用。这将重复进行,保证规则仅对同一对象应用一次(如果未从文档中完全删除并再次添加,请参阅下面关于文档成员资格的章节),直到不再发生应用为止。因此,可以在规则应用期间添加元素,然后在以后由同一规则或另一规则处理。
该库使用 SwiftXMLParser 来解析 XML,后者实现了来自 SwiftXMLInterfaces 的相应协议。
根据解析过程的配置,XML 源的所有部分都可以保留在 XML 文档中,包括所有注释和内部子集的部分,例如所有实体或元素定义。(元素定义和属性列表定义,除了它们报告的元素名称外,仅保留为其原始文本表示形式,它们不会被解析为任何其他表示形式。)
在当前的实现中,XML 库不实现任何验证,即针对 DTD 或其他 XML 模式的验证,例如,告诉我们是否可以将具有特定名称的元素包含在具有另一个特定名称的元素中。用户必须使用其他库(例如 Libxml2Validation)在读取或写入文档之前或之后进行此类验证。除了验证 XML 文档的结构外,验证对于了解空白文本的出现是否重要(即是否应保留)也很重要。(例如,表示文本文档段落的元素之间的空白文本通常被认为是不重要的。)为了弥补最后一个问题,库的用户可以提供一个函数来决定是否应保留元素之间的空白文本实例。此外,如果需要,一旦构建了文档树,用户必须设置属性的可能默认值。
此库完全控制如何处理实体。即使未定义,命名实体引用也可以在文档内部持久存在。命名实体引用在解析期间被评分为内部或外部实体引用,外部实体引用是由文档声明内部子集中的外部实体定义引用的那些实体引用。可以根据内部子集和/或由应用程序控制自动替换内部实体引用为文本。
可以配置自动包含外部解析实体的内容,然后内容可能会被具有实体相应信息的元素包装。
带有命名空间前缀的元素或属性被赋予全名“prefix:unprefixed”。请参阅关于处理命名空间的章节,了解动机以及如何处理命名空间。
对于解析期间的任何错误,都会抛出错误,并且不会提供文档。
不得并发检查或更改 XML 树(例如文档)。
注意
以下库的描述可能不包括所有类型和方法。请参阅 DocC 生成的文档或在相应的集成开发环境 (IDE) 中使用自动完成功能。
以下函数接受源并返回 XML 文档实例 (XDocument
)。源可以作为 URL、文件路径、文本或二进制数据提供。
从引用本地文件的 URL 读取
func parseXML(
fromURL: URL,
registeringAttributes attributeRegisterMode: AttributeRegisterMode = .none,
sourceInfo: String? = nil,
textAllowedInElementWithName: ((String) -> Bool)? = nil,
internalEntityAutoResolve: Bool = false,
internalEntityResolver: InternalEntityResolver? = nil,
internalEntityResolverHasToResolve: Bool = true,
insertExternalParsedEntities: Bool = false,
externalParsedEntitySystemResolver: ((String) -> URL?)? = nil,
externalParsedEntityGetter: ((String) -> Data?)? = nil,
externalWrapperElement: String? = nil,
keepComments: Bool = false,
keepCDATASections: Bool = false,
eventHandlers: [XEventHandler]? = nil
) throws -> XDocument
并相应地
func parseXML(
fromPath: String,
...
) throws -> XDocument
func parseXML(
fromText: String,
...
) throws -> XDocument
func parseXML(
fromData: Data,
...
) throws -> XDocument
如果您想对要处理的源的类型漠不关心,请使用 XDocumentSource
进行源定义并使用
func parseXML(
from: XDocumentSource,
...
) throws -> XDocument
可选的 textAllowedInElementWithName
方法在元素内部找到文本时获取周围元素的名称,并应通知是否允许在特定上下文中出现文本。如果不允许,则文本将被丢弃(如果它是空白)。如果在此上下文中不允许文本,但文本不是空白,则会抛出错误。如果您需要比元素名称更具体的上下文来决定是否允许文本,请使用 XEventHandler
来跟踪更具体的上下文信息。
属性值中的所有内部实体引用都必须在解析期间替换为文本。为了实现这一点(以防内部实体引用出现在源的属性值中),可以提供 InternalEntityResolver
。InternalEntityResolver
必须实现以下方法
func resolve(
entityWithName: String,
forAttributeWithName: String?,
atElementWithName: String?
) -> String?
当遇到命名实体引用时(在文本或属性中),此方法始终被调用,该实体引用被评分为内部实体。它要么返回实体的文本替换,要么通过返回 nil
来不解析实体。默认情况下,解析器必须解析呈现给它的所有实体,否则会抛出相应的错误。您可以通过在解析函数的调用中设置 internalEntityResolverHasToResolve: false
来移除此强制执行;然后,当解析器返回 nil
时,实体引用不会被文本替换,而是会被保留,而不会有任何进一步的通知。在属性值中的命名实体的情况下,当未给出替换时,始终会抛出错误。如果实体在属性值内部遇到,则函数参数 forAttributeWithName
(属性的名称)和 atElementWithName
(元素的名称)具有相应的值。
如果 internalEntityAutoResolve
设置为 true
,则解析器首先尝试使用文档内部子集中的声明来替换内部实体,然后再调用 InternalEntityResolver
。
默认情况下,不插入外部解析实体的内容,但如果您将 insertExternalParsedEntities
设置为 true
,则会插入。您可以在参数 externalParsedEntitySystemResolver
中提供一个方法,将外部解析实体的系统标识符解析为 URL。您还可以在参数 externalParsedEntityGetter
中提供一个方法来获取系统标识符的数据(如果提供了 externalParsedEntitySystemResolver
,则 externalParsedEntitySystemResolver
必须首先返回 nil
)。最后,系统标识符仅作为路径组件添加到源 URL(如果存在),并且解析器尝试从那里加载实体。
当插入外部解析实体的内容时,您可以声明一个元素名称 externalWrapperElement
:然后插入的内容将被包装到一个具有该名称的元素中,并在属性 name
、systemID
和 path
中包含有关实体的信息(path
是可选的,因为外部解析实体可能在没有显式路径的情况下被解析)。(在以后的处理中,您可能希望更改此表示形式,例如,如果外部解析实体引用是元素的唯一内容,则您可以用其内容替换包装器,并将相应的信息设置为父元素的一些附件,以便文档验证成功。)
可以向 parseXML
调用提供一个或多个事件处理程序,这些处理程序实现来自 XMLInterfaces 的 XEventHandler
。这允许库的用户捕获解析期间的任何事件,例如进入或离开元素。例如,内部实体引用的解析可能取决于文档内部的位置(而不仅仅是元素的名称或属性),因此可以通过这样的事件处理程序收集此信息。
keepComments
(默认值:false
)决定是否应保留注释(作为 XComment
),否则它们将被丢弃,恕不另行通知。keepCDATASections
(默认值:false
)决定是否应保留 CDATA 部分(作为 XCDATASection
),否则所有 CDATA 部分都将解析为文本。
XML 文档 (XDocument
) 可以包含以下内容
XElement
:一个元素XText
:文本XInternalEntity
:内部实体引用XExternalEntity
:外部实体引用XCDATASection
:CDATA 部分XProcessingInstruction
:处理指令XComment
:注释XLiteral
:包含旨在“按原样”序列化的文本,即不进行转义,例如 <
和 &
,它可能包含要字面序列化的 XML 代码,因此得名XLiteral
永远不是解析 XML 的结果,但可能会被应用程序添加。后续的 XLiteral
内容(就像 XText
一样,请参阅关于处理文本的章节)始终会自动合并。
这些内容是 XContent
类型,而更通用的类型 XNode
可能是内容或 XDocument
。
以下内容从内部子集中读取
XInternalEntityDeclaration
:内部实体声明XExternalEntityDeclaration
:外部实体声明XUnparsedEntityDeclaration
:未解析的外部实体声明XNotationDeclaration
:符号声明XParameterEntityDeclaration
:参数实体声明XElementDeclaration
:元素声明XAttributeListDeclaration
:属性列表声明可以通过属性 declarationsInInternalSubset
访问它们。
文档从 XML 源获取以下附加属性(某些值可能为 nil
)
encoding
:来自 XML 声明的编码publicID
:来自文档类型声明的公共标识符sourcePath
:XML 文档的源路径standalone
:来自 XML 声明的独立值systemID
:来自文档类型声明的系统标识符xmlVersion
:来自 XML 声明的 XML 版本当未在 XML 源中显式设置时,其中一些值将设置为合理的值。
当通过 print(...)
打印内容时,仅打印顶级表示形式,如开始标记,而永远不会打印整个树。当您想打印整个树或文档时,请使用
func echo(pretty: Bool, indentation: String, terminator: String)
pretty
默认为 false
;如果设置为 true
,则添加换行符和空格以进行美观打印。indentation
默认为两个空格,terminator
默认为 "\n"
,即在输出后打印换行符。
具有更多控制
func echo(usingProductionTemplate: XProductionTemplate, terminator: String)
将在下一节中解释生成。
当您想要将整个树或文档序列化为文本 (String
) 时,请使用以下方法
func serialized(pretty: Bool) -> String
pretty
再次默认为 false
并具有相同的效果。
具有更多控制
func serialized(usingProductionTemplate: XProductionTemplate) -> String
不要使用 serialized
打印树或文档,而应使用 echo
,因为在这种情况下使用 echo
更有效。
可以写入任何 XML 节点(包括 XML 文档),包括由它开始的节点树,通过以下方法。
func write(toURL: URL, usingProductionTemplate: XProductionTemplate) throws
func write(toPath: String, usingProductionTemplate: XProductionTemplate) throws
func write(toFile: FileHandle, usingProductionTemplate: XProductionTemplate) throws
func write(toWriter: Writer, usingProductionTemplate: XProductionTemplate) throws
您还可以使用 WriteTarget
协议来允许上述所有可能性
func write(to writeTarget: WriteTarget, usingProductionTemplate: XProductionTemplate) throws
通过参数 usingProductionTemplate:
,您可以定义一个生成,即序列化的详细信息,例如是否插入换行符以使结果看起来美观。其值默认为 XActiveProductionTemplate
的实例,这将给出标准输出。
这种生成的定义分为两个部分,一个模板,可以使用值初始化该模板,以进一步配置序列化,以及一个活动生成,该活动生成要应用于特定目标。这样,用户就能够完全定义序列化的外观,然后将此定义应用于一个或多个序列化。更详细地说
XProductionTemplate
具有方法 activeProduction(for writer: Writer) -> XActiveProduction
,它通过使用 writer
初始化 XActiveProduction
,其中相应的事件触发写入 writer
。此类生成的配置应通过 XProductionTemplate
的初始化器的参数提供。
因此,XActiveProduction
定义了如何写入文档的每个部分,例如,是否将 >
或 "
按字面写入,还是作为文本部分中预定义的 XML 实体写入。上述函数调用中的生成默认为 DefaultProductionTemplate
的实例,这将导致 ActiveDefaultProduction
的实例。如果仅要更改文档写入方式的某些细节,则应扩展 ActiveDefaultProduction
。生成 ActivePrettyPrintProduction
(可以通过定义 PrettyPrintProductionTemplate
来使用)和 ActiveHTMLProduction
(可以通过定义 HTMLProductionTemplate
来使用)已经扩展了 ActiveDefaultProduction
,可以用于美观打印 XML 或输出 HTML。(请注意,HTMLProductionTemplate
可以给定 NamespaceReference
以考虑 HTML 元素的可能命名空间前缀。)但是您也可以自己扩展这些类之一,例如,您可以覆盖 func writeText(text: XText)
和 func writeAttributeValue(name: String, value: String, element: XElement)
以再次将某些字符写为命名实体引用。或者,您只需提供 DefaultProduction
本身的实例,并更改其 linebreak
属性以定义应如何写入换行符(例如 Unix 或 Windows 样式)。您可能还想考虑 func sortAttributeNames(attributeNames: [String], element: XElement) -> [String]
以对输出的属性进行排序。
示例:在所有元素之前写入换行符
class MyProduction: DefaultProduction {
override func writeElementStartBeforeAttributes(element: XElement) throws {
try write(linebreak)
try super.writeElementStartBeforeAttributes(element: element)
}
}
try document.write(toFile: "myFile.xml", usingProduction: MyProduction())
为了通用性,提供了以下方法,将任何 XActiveProduction
应用于节点及其包含的树
func applyProduction(activeProduction: XActiveProduction) throws
可以使用以下方法克隆任何节点(包括 XML 文档),包括以它为根的节点树
var clone: XNode
(如果主题已知更具体,则结果将更具体。)
任何内容和文档本身都拥有属性 backlink
,可以用作克隆和原始节点之间的关系。如果您使用 clone
属性创建克隆,则克隆中节点的 backlink
值指向原始节点。因此,当使用克隆时,您可以轻松查看原始节点。
(回溯链接也可以通过方法 setting(backlink:)
或 copyingBacklink(from:)
手动设置,这在转换中可能会派上用场。)
请注意,backlink
引用弱引用原始节点,即如果您不保存对原始节点或树的引用,则原始节点将消失,并且 backlink
属性将为 nil
。
如果您想使用克隆仅将文档的版本保存到副本,请使用其以下方法
func makeVersion()
在这种情况下,将创建文档的克隆,但原始节点的 backlink
属性指向克隆,并且克隆的 backlink
属性将指向原始节点的旧 backlink
值。即,如果您多次应用 saveVersion()
,则在从原始文档中的节点开始跟踪 backlink
值时,您将遍历此节点的所有版本,从较新的版本到较旧的版本。backlinks
属性为您提供正是回溯链接链。与使用 clone
不同,文档将记住对此文档版本的强引用,因此将保留克隆的节点。在文档上使用 forgetVersions(keeping:Int)
以停止此记忆,仅保留由参数 keeping
定义的最后版本数(keeping
默认为 0)。在仍然记住的最旧版本中,或者如果没有留下记住的版本,则在文档本身中,所有 backlink
值都将设置为 nil
。
finalBacklink
属性跟踪整个 backlink
值链,并为您提供此链中的最后一个值。
有时,只需要“浅”克隆,即节点本身,而无需以该节点为根的整个节点树。在这种情况下,只需使用
func shallowClone(forwardref: Bool) -> XNode
然后,backlink
的设置方式与使用 clone
时相同。
属性 backlinkOrSelf
提供回溯链接,或者——如果它是 nil
——则提供主题本身。
如果解析器(正如 SwiftXMLParser 的情况一样)报告了文档的哪个部分在文本中(即它在第几行和第几列开始,在第几行和第几列结束),则属性 sourceRange: XTextRange
(使用来自 SwiftXMLInterfaces 的 XTextRange
)为相应的节点返回它
示例
let document = try parseXML(fromText: """
<a>
<b>Hello</b>
</a>
""", textAllowedInElementWithName: { $0 == "b" })
for content in document.allContent {
if let sourceRange = content.sourceRange {
print("\(sourceRange): \(content)")
}
else {
content.echo()
}
}
输出
1:1 - 3:4: <a>
2:5 - 2:16: <b>
2:8 - 2:12: Hello
可以使用元素的属性 name
读取和设置元素名称。在设置与现有名称不同的新名称后,如果元素是文档的一部分,则该元素将在文档中以新名称注册。设置相同的名称不会更改任何内容(这是一种有效的非更改)。
对于文本内容 (XText
),可以通过其 value
属性读取和设置文本。因此,无需通过替换另一个 XText
内容来更改文本。另请参阅下面关于处理文本的部分。
元素的属性可以通过“索引符号”读取和设置。如果未设置属性,则返回 nil
;反之,将属性设置为 nil
会将其删除。如果元素是文档的一部分,则设置具有新名称的属性或删除属性会更改文档中属性的注册。将已存在属性的非 nil
值设置为属性,对于属性的注册来说,是一种高效的非更改操作。
示例
// setting the "id" attribute to "1":
myElement["id"] = "1"
// reading an attribute:
if let id = myElement["id"] {
print("the ID is \(id)")
}
您还可以从元素序列中获取属性值(可选字符串)的序列。
示例
let document = try parseXML(fromText: """
<test>
<b id="1"/>
<b id="2"/>
<b id="3"/>
</test>
""")
print(document.children.children["id"].joined(separator: ", "))
结果
1, 2, 3
如果您想获取属性值并同时删除该属性,请使用元素的 pullAttribute(...)
方法。
要获取元素的所有属性名称,请使用
var attributeNames: [String]
请注意,您还可以通过使用相同的索引符号来获取(惰性)元素序列的特定属性名称的属性值(惰性)序列
print(myElement.children("myChildName")["myAttributeName"].joined(separator: ", "))
所有节点都可以拥有“附件”。这些是通过文本键附加的对象。这些附件不被视为属于正式的 XML 树。
这些附件以字典 attached
的形式实现,作为每个节点的成员。
您还可以在创建元素或文档时立即设置附件,方法是使用初始化器的 attached:
参数。(请注意,在此参数中,为了方便起见,某些值可能是 nil
。)
通过以下方式获取节点的 XPath
var xPath: String
从节点(包括文档)开始的深度优先遍历可以通过以下方法完成
func traverse(down: (XNode) throws -> (), up: ((XNode) throws -> ())? = nil) rethrows
func traverse(down: (XNode) async throws -> (), up: ((XNode) async throws -> ())? = nil) async rethrows
对于“分支”,即可能包含其他节点的节点(例如元素,与不包含其他节点的文本相对),当从其内容的遍历返回时(也包括空分支的情况),将调用给定可选 up:
参数的闭包。
示例
document.traverse { node in
if let element = node as? XElement {
print("entering element \(element.name)")
}
}
up: { node in
if let element = node as? XElement {
print("leaving element \(element.name)")
}
}
请注意,在遍历期间不得删除遍历的根节点。
如前所述和一般描述,该库允许在文档中高效查找具有特定名称的元素,而无需遍历整个树。
查找具有特定名称的元素
func elements(_: String) -> XElementsOfSameNameSequence
示例
for paragraph in myDocument.elements("paragraph") {
if let id = paragraph["id"] {
print("found paragraph with ID \"\(ID)\"")
}
}
通过在 elements(_:)
中使用多个名称来查找具有多个名称备选项的元素。请注意,与单个名称的方法一样,您在迭代期间添加的内容也将被考虑在内。
要直接查找在何处设置了具有特定名称的属性,您可以使用类似于直接访问元素的方法,但出于效率原因,您必须指定可用于此类直接访问的属性名称。您可以在创建文档时指定这些属性名称(例如 XDocument(registeringAttributes: .selected(["id", "label"]))
)或在使用解析函数时间接指定(例如 try parseXML(fromText: "...", registeringAttributes: .selected(["id", "label"]))
)。
示例
let document = try parseXML(fromText: """
<test>
<x a="1"/>
<x b="2"/>
<x c="3"/>
<x d="4"/>
</test>
""", registeringAttributes: .selected(["a", "c"]))
let registeredValuesInfo = document.registeredAttributes("a", "b", "c", "d").map{ "\($0.name)=\"\($0.value)\" in \($0.element)" }.joined(separator: ", ")
print(registeredValuesInfo) // "a="1" in <x a="1">, c="3" in <x c="3">"
let allValuesInfo = document.elements("x").compactMap{
if let name = $0.attributeNames.first, let value = $0[name] { "\(name)=\"\(value)\" in \($0)" } else { nil }
}.joined(separator: ", ")
print(allValuesInfo) // "a="1" in <x a="1">, b="2" in <x b="2">, c="3" in <x c="3">, d="4" in <x d="4">"
从某些内容开始,您可能想要查找相关内容,例如其子项。为相应方法选择的名称来自以下想法:所有内容都具有自然的顺序,即深度优先遍历的顺序,这与 XML 文档的内容存储在文本文件中的顺序相同。此顺序为方法名称(例如 nextTouching
)赋予了意义。请注意,与您通过 elements(_:)
获取的迭代不同,即使停留在同一文档中的节点,如果在迭代期间相应地移动,也可能在这样的迭代中多次出现。
返回的序列始终是惰性序列,迭代它们会给出显而易见的类型的项目。正如库的一般描述中所提到的,允许在这样的迭代期间操作 XML 树。
查找节点所在的文档
var document: XDocument?
查找父元素
var parent: XElement?
其所有祖先元素
var ancestors: XElementSequence
获取分支的第一个内容
var firstContent: XContent?
获取分支的最后一个内容
var lastContent: XContent?
如果只包含一个节点,则获取它,否则获取 nil
var singleContent: XContent?
文档或元素的直接内容(“直接”表示它们的父级是此文档或元素)
var content: XContentSequence
作为元素的直接内容,即所有子元素
var children: XElementSequence
作为文本的直接内容
var immediateTexts: XTextSequence
对于 content
和 children
序列,还存在序列 contentReversed
、childrenReversed
和 immediateTextsReversed
,它们从最后一个对应的项目迭代到第一个。
节点本身开始的节点树中的所有内容,不包括节点本身,按照深度优先遍历的顺序
var allContent: XContentSequence
节点开始的节点树中的所有内容,包括节点本身
var allContentIncludingSelf: XContentSequence
树中的所有文本
var allTexts: XTextSequence
后代,即节点本身开始的节点树中的所有内容,不包括节点本身,并且是元素
var descendants: XElementSequence
如果节点是一个元素,则元素本身和后代,从元素本身开始
var descendantsIncludingSelf: XElementSequence
节点本身开始的节点树中的所有文本,不包括节点本身,按照深度优先遍历的顺序
var allTexts: XTextSequence
相同,但仅适用于作为直接内容包含的节点
var immediateTexts: XTextSequence
分支(元素或文档)的(直接)内容彼此是“兄弟姐妹”。
主题之前的内容项
var previousTouching: XContent?
主题之后的内容项
var nextTouching: XContent?
(请注意,为了自动完成,最好输入“touch...”而不是“prev...”或“next...”。)
您可能只想知道是否存在上一个或下一个节点
var hasPrevious: Bool
var hasNext: Bool
以下非常短的方法名称 previous
和 next
实际上意味着“上一个内容”和“下一个内容”。之所以选择如此短的方法名称,是因为它们是非常常见的用例。
节点之前的所有节点(即上一个兄弟节点),在同一级别上,即具有相同的父节点,从节点开始排序
var previous: XContentSequence
其中,作为元素的节点
var previousElements: XElementSequence
类似地,节点之后的内容
var next: XContentSequence
其中,作为元素的节点
var nextElements: XElementSequence
nextElements
和 previousElements
跳过任何非元素。如果您想查找中间没有非元素的元素,请使用 nextCloseElements
和 previousCloseElements
。
注意
请记住,没有 ...Close...
的版本只是忽略中间的所有其他节点类型。
还有 ...IncludingSelf
版本,其中包含主题本身,例如 nextElementsIncludingSelf
。
示例
for descendant in myElement.descendants {
print("the name of the descendant is \(descendant.name)")
}
请注意,一个序列可能会被多次使用
let document = try parseXML(fromText: """
<a><c/><d/><e/></a>
""")
let insideA = document.children.children
insideA.echo()
print("again:")
insideA.echo()
输出
<c/>
<d/>
<e/>
again:
<c/>
<d/>
<e/>
一旦您有了这样的序列,您就可以通过其 first
属性获取序列中的第一个项目(这是此包除了已定义的 first(where:)
之外引入的)。
可以使用序列的常用方法。例如,使用 mySequence.dropFirst(n)
删除序列 mySequence
的前 n
个项目。例如,要获取序列的第三个项目,请使用 mySequence.dropFirst(2).first
。
请注意,没有属性可以获取这些序列的最后一个项目,因为这将非常低效。最好将 contentReversed
或 childrenReversed
与 first
结合使用。
使用 exist
测试序列中是否存在某些内容
var exist: Bool
请注意,在使用 exist
后,您仍然可以沿着同一序列正常迭代,而不会丢失任何项目。
使用 absent
测试序列中是否不存在任何内容
var absent: Bool
如果您想测试某些项目是否存在,并且在许多情况下您也会使用这些项目。内容或元素序列的属性 existing
在项目存在时返回序列本身,否则返回 nil
var existing: XContentSequence?
var existing: XElementSequence?
在以下示例中,首先测试序列中是否存在项目,如果存在项目,则使用该序列
let document = try parseXML(fromText: """
<a><c/><b id="1"/><b id="2"/><d/><b id="3"/></a>
""")
if let theBs = document.descendants("b").existing {
theBs.echo()
}
请注意,您通过使用 existing
获取的仍然是惰性序列,即,如果您在 existing
测试和使用其结果之间更改内容,则可能不再有项目可以找到。
您还可以按深度优先遍历的顺序,询问树中上一个或下一个内容项。例如,如果一个节点是以某个元素为根的子树的最后一个节点,并且该元素有下一个兄弟节点,则该下一个兄弟节点是该子树的最后一个节点的“树中的下一个节点”。获取树中的下一个或上一个节点非常高效,因为库无论如何都会跟踪它们。
树中的下一个内容项
var nextInTreeTouching: XContent?
树中的上一个内容项
var previousInTreeTouching: XContent?
查找节点树中包含的所有文本,并将它们组合成一个 String
var allTextsCombined: String
即使您知道只有一个文本要“组合”,也可以使用这些文本收集属性,这种情况已得到高效实现。
您还可以使用以下方法将单个内容项,或更具体地说,将元素转换为适当的序列
对于任何内容
var asSequence: XContentSequence
对于元素
var asElementSequence: XElementSequence
(这两种方法在库的测试中使用。)
上一节中返回序列的所有方法也允许将条件作为第一个参数进行过滤。我们区分以下情况:序列的所有项目都满足条件,当条件满足时所有项目,以及直到条件满足时所有项目(不包括条件满足时找到的项目)
func content((XContent) -> Bool) -> XContentSequence
func content(while: (XContent) -> Bool) -> XContentSequence
func content(until: (XContent) -> Bool) -> XContentSequence
func content(untilAndIncluding: (XContent) -> Bool) -> XContentSequence
untilAndIncluding
版本也在条件满足时停止,但包括相应的项目。
在合理的情况下,会返回更具体类型的序列。
示例
let document = try parseXML(fromText: """
<a><b/><c take="true"/><d/><e take="true"/></a>
""")
for descendant in document.descendants({ element in element["take"] == "true" }) {
print(descendant)
}
输出
<c take="true">
<e take="true">
请注意,示例中条件周围的圆括号“(...)”是必需的,以便将其与 while:
和 until:
版本区分开来。(没有 where:
参数名称,因为没有它,不太常见的 while:
– 以及在较小程度上 until:
– 更容易在视觉上与它区分开来,更常见的情况在语法上是最短的。这在实际代码中效果很好。)
还存在根据名称过滤元素的常用快捷方式
for _ in document.descendants("paragraph") {
print("found a paragraph!")"
}
您还可以使用多个名称(例如 descendants("paragraph", "table")
)。如果未给出名称,则结果中将给出所有元素,而与名称无关,例如 children()
与 children
含义相同。
节点
请注意,nextElements("paragraph")
(按名称过滤下一个元素)与 nextElements(while: { $0.name == "paragraph" })
不同。
如果您知道最多只有一个具有特定名称的子元素,请使用以下方法(如果存在,它将返回具有此名称的第一个子元素)
func firstChild(_ name: String) -> XElement?
然后您还可以考虑备用名称(为您提供名称匹配的第一个子元素)
func firstChild(_ names: String...) -> XElement?
如果您想获取具有特定名称的第一个祖先,请使用以下方法之一
func ancestor(_ name: String) -> XElement?
func ancestor(_ names: String...) -> XElement?
迭代器也可以链接在一起。第二个迭代器在第一个迭代器遇到的每个节点上执行。所有这些迭代都是惰性的,因此只有当第二个迭代器完成第一个迭代器找到的当前节点时,第一个迭代器才会搜索下一个节点。
示例
let document = try parseXML(fromText: """
<a>
<b>
<c>
<d/>
</c>
</b>
</a>
""")
for element in document.descendants.descendants { print(element) }
输出
<b>
<c>
<d>
<c>
<d>
<d>
此外,在应用于单个节点(如 parent
)的链式操作中,查找单个节点的操作也有效,您可以使用例如 insertNext
(请参阅关于树操作的部分)或 with
(请参阅下一节关于构造 XML 的部分)或 echo()
。
当使用带有 String
的索引时,您将获得相应属性值(设置的位置)的序列
for childID in element.children["id"] {
print("found child ID \(childID)")
}
请注意,当将 Int
用作内容序列的下标值时,您将获得相应索引的子项
if let secondCHild = element.children[2] {
print("second child: \(secondChild)")
}
注意
如果您对 XContent、XElement 或 XText 序列使用此下标表示法 [n]
,那么 – 尽管使用了整数值 – 这不是 (!) 对元素的随机访问(每次使用这样的下标时,都会跟踪序列,直到通过计数找到相应的项目),并且计数从 1 开始,就像在 XPath 语言中一样,而不是像例如 Swift 数组那样从 0 开始。
您应该将此整数下标更多地视为带有名称的下标,整数值是位置在 XML 中给出的名称,其中从 1 开始计数是常见的。
在构造元素(没有内容)时,名称作为第一个(无名)参数给出,属性值作为(无名)字典给出。
示例:构造一个带有属性 id="1"
和 style="note"
的空“paragraph”元素
let myElement = XElement("paragraph", ["id": "1", "style": "note"])
在我们详细解释相应的功能之前,我们首先想给出一些重要的提示。
请注意,当将内容插入到元素或文档中,并且该内容已存在于其他位置时,插入的内容是从其原始位置移动,而不是复制。如果您想插入副本,请插入使用内容的 clone
属性的结果。
在编写代码时要“大胆”,可能比您想象的更多功能可以工作。例如,预测以下各节中的解释,以下代码示例确实有效
将元素的“a”子元素和“b”子元素移动到元素的开头
element.addFirst {
element.children(“a”)
element.children(“b”)
}
由于内容是先构造然后再插入,因此这里没有无限循环。
请注意,在结果中,内容的顺序就像在括号 {...}
内定义的那样,因此在示例中,在生成的 element
内部,首先是“a”子元素,然后是“b”子元素。
用另一个元素包装一个元素
element.replace {
XElement("wrapper") {
element
}
}
您在括号 {...}
内定义的内容是从内向外构造的。从上面的注释中,您可能会认为示例中的 element
在“wrapper”元素的内容被构造时不再在其原始位置,在实际发生替换之前。是的,这是真的,但尽管如此,replace
方法仍然知道在哪里插入这个“wrapper”元素。该操作确实像您从朴素的角度期望的那样工作。
任何符合 XContentConvertible
类型的实例(它必须实现其 collectXML(by:)
方法)都可以作为 XML 插入
struct MyStruct: XContentConvertible {
let text1: String
let text2: String
func collectXML(by xmlCollector: inout XMLCollector) {
xmlCollector.collect(XElement("text1") { text1 })
xmlCollector.collect(XElement("text2") { text2 })
}
}
let myStruct1 = MyStruct(text1: "hello", text2: "world")
let myStruct2 = MyStruct(text1: "greeting", text2: "you")
let element = XElement("x") {
myStruct1
myStruct2
}
element.echo(pretty: true)
结果
<x>
<text1>hello</text1>
<text2>world</text2>
<text1>greeting</text1>
<text2>you</text2>
</x>
对于 XContentConvertible
,还有 xml
属性,它返回一个 XContent
的相应数组。
在构造元素时,其内容在括号 {...}
中给出(这些括号是初始化器的 builder
参数)。
let myElement = XElement("div") {
XElement("hr")
XElement("paragraph") {
"Hello World"
}
XElement("hr")
}
(文本 "Hello World"
也可以作为 XText("Hello World")
给出。文本将在这样的 XML 节点中自动转换。)
内容可以作为数组或适当的序列给出
let myElement = XElement("div") {
XElement("hr")
myOtherElement.content
XElement("hr")
}
在不定义内容时,使用 map
可能是一个明智的选择
let element = XElement("z") {
XElement("a") {
XElement("a1")
XElement("a2")
}
XElement("b") {
XElement("b1")
XElement("b2")
}
}
for content in element.children.map({ $0.children.first }) { print(content?.name ?? "-") }
输出
a1
b1
这同样适用于例如 filter
方法,除了让代码看起来更复杂(当用作代替上面描述的过滤器选项时),在定义内容时不是一个好的选择。
在定义内容时,包含其他元素的元素的内容是从内向外构建的:考虑以下示例
let b = XElement("b")
let a = XElement("a") {
b
"Hello"
}
a.echo(pretty: true)
print("\n------\n")
b.replace {
XElement("wrapper1") {
b
XElement("wrapper2") {
b.next
}
}
}
a.echo(pretty: true)
首先,构建元素“wrapper2”,此时序列 b.next
包含文本 "Hello"
。因此,我们将得到以下输出
<a><b/>Hello</a>
------
<a>
<wrapper1>
<b/>
<wrapper2>Hello</wrapper2>
</wrapper1>
</a>
作为文档 (XDocument
) 一部分的元素在文档中注册。原因是这允许通过 elements(_:)
分别 attributes(_:)
快速访问特定名称的元素或属性,并且规则(请参阅关于规则的部分)使用这些注册(请注意,出于效率原因,要以这种方式使用的属性名称必须在创建文档时配置)。
在构造新元素时,其内容在构造期间的 {...}
括号中定义,该元素不属于任何文档。插入其中的节点离开文档树,但它们不会 (!) 从文档中注销。即,迭代 elements(_:)
仍会找到它们,并且相应的规则将应用于它们。这种行为的原因是新元素插入到同一文档的常见情况。如果新元素的内容首先从文档中注销,然后再重新插入到同一文档中,则它们将被视为新元素,并且提到的迭代可能会再次迭代它们。
如果您想让新构建元素的内容从文档中注销,请使用其方法 adjustDocument()
。此方法将元素的当前文档扩散到其内容。对于新构建的元素,此文档是 nil
,这会将节点从其文档中注销。您还可以在元素的初始化器中将属性 adjustDocument
设置为 true
,以便在新元素的构建完成时自动调用 adjustDocument()
。此调用或设置为调整文档仅在顶层元素上是必要的,它会分散到整个树中。
请注意,如果您将元素插入到作为文档一部分的另一个文档中,则如果新子元素尚未在其新父元素的文档中注册,则会注册在该文档中(并从之前注册的任何不同文档中注销)。
示例:将新构造的元素添加到文档
let document = try parseXML(fromText: """
<a><b id="1"/><b id="2"/></a>
""")
for element in document.elements("b") {
print("applying the rule to \(element)")
if element["id"] == "2" {
element.insertNext {
XElement("c") {
element.previous
}
}
}
}
print("\n-----------------\n")
document.echo()
输出
applying the rule to <b id="1">
applying the rule to <b id="2">
-----------------
<a><b id="2"/><c><b id="1"/></c></a>
正如您可以从最后一个示例中的 print
命令中看到的那样,元素 <b id="1">
不会失去其与文档的“连接”(尽管它似乎又被添加到文档中),因此它在迭代中只被迭代一次。
除了更改节点属性外,还可以通过以下方法更改 XML 树。其中一些方法返回主题本身作为可丢弃的结果。对于在 {...}
(构建器)中指定的内容,顺序会被保留。
分别在元素或文档的内容末尾添加节点
func add(builder: () -> [XContent])
分别在元素或文档的内容开头添加节点
func addFirst(builder: () -> [XContent])
将节点添加为节点之前的节点
func insertPrevious(_ insertionMode: InsertionMode = .following, builder: () -> [XContent])
将节点添加为节点之后的节点
func insertNext(_ insertionMode: InsertionMode = .following, builder: () -> [XContent])
如果主题的类型更精确地已知,则从 insertPrevious
和 insertNext
返回更精确的类型。
通过使用接下来的两种方法,节点将被删除。
从树结构和文档中删除节点
func remove()
您还可以使用节点的 removed()
方法来删除节点,但也使用该节点。
用其他节点替换节点
func replace(_ insertionMode: InsertionMode = .following, builder: () -> [XContent])
请注意,替换节点的内容允许包含节点本身。
分别清除元素或文档的内容
func clear()
测试元素或文档是否为空
var isEmpty: Bool
分别设置元素或文档的内容
func setContent(builder: () -> [XContent])
示例
for table in myDocument.elements("table") {
table.insertNext {
XElement("legend") {
"this is the table legend"
}
XElement("caption") {
"this is the table caption"
}
}
}
请注意,默认情况下,迭代会继续,并且新插入的节点(由 insertPrevious
或 insertNext
插入)也会被考虑在内。在以下情况下,您必须添加 .skipping
指令才能获得如下所示的输出(在第二种情况下,如果您不设置 .skipping
,甚至会得到无限循环)
let element = XElement("top") {
XElement("a1") {
XElement("a2")
}
XElement("b1") {
XElement("b2")
}
XElement("c1") {
XElement("c2")
}
}
element.echo(pretty: true)
print("\n---- 1 ----\n")
for content in element.content {
content.replace(.skipping) {
content.content
}
}
element.echo(pretty: true)
print("\n---- 2 ----\n")
for content in element.contentReversed {
content.insertPrevious(.skipping) {
XElement("I" + ((content as? XElement)?.name ?? "?"))
}
}
element.echo(pretty: true)
输出
<top>
<a1>
<a2/>
</a1>
<b1>
<b2/>
</b1>
<c1>
<c2/>
</c1>
</top>
---- 1 ----
<top>
<a2/>
<b2/>
<c2/>
</top>
---- 2 ----
<top>
<Ia2/>
<a2/>
<Ib2/>
<b2/>
<Ic2/>
<c2/>
</top>
请注意,当不使用 insertPrevious
、insertNext
或 replace
(例如,当使用 add
时)时,没有这种跳过插入内容的机制。考虑组合 descendants.add
:那么就没有“自然”的方法来纠正树的遍历。(更常见的用例可能是类似 descendants("table").add { XElement("caption") }
的情况,因此这在常见情况下不应该是一个问题,但您应该意识到这一点。)
当在链式迭代器中使用 insertNext
、replace
等时,会发生的情况是,括号 {...}
中内容的定义会为序列中的每个项目执行。您可能应该使用 collect
函数来专门为当前项目构建内容。例如,在最后一个示例中,您可以使用相同的结果
print("\n---- 1 ----\n")
element.content.replace { content in
collect {
content.content
}
}
element.echo(pretty: true)
print("\n---- 2 ----\n")
element.contentReversed.insertPrevious { content in
find {
XElement("I" + ((content as? XElement)?.name ?? "?"))
}
}
element.echo(pretty: true)
您也可以不使用 collect
let e = XElement("a") {
XElement("b")
XElement("c")
}
for descendant in e.descendants({ $0.name != "added" }) {
descendant.add { XElement("added") }
}
e.echo(pretty: true)
输出
<a>
<b>
<added/>
</b>
<c>
<added/>
</c>
</a>
请注意,每次都会创建一个新的 <added/>
。从已经说过的内容来看,应该清楚的是,这种“重复”不适用于现有内容(除非您使用 clone
或 shallowClone
)
let myElement = XElement("a") {
XElement("to-add")
XElement("b")
XElement("c")
}
for descendant in myElement.descendants({ $0.name != "to-add" }) {
descendant.add {
myElement.descendants("to-add")
}
}
myElement.echo(pretty: true)
输出
<a>
<b/>
<c>
<to-add/>
</c>
</a>
作为一般规则,当插入内容时,如果该内容已经是另一个元素或文档的一部分,则该内容不会被复制,而是从其原始位置删除。
当您确实希望内容被复制时,请使用 clone
(或 shallowClone
),例如,在最后一个示例中使用 myElement.descendants("to-add").clone
将输出
<a>
<to-add/>
<b>
<to-add/>
</b>
<c>
<to-add/>
<to-add/>
</c>
</a>
默认情况下,当您插入内容时,此新内容也会被跟踪(插入模式 .following
),因为这最能反映此库的动态特性。如果您不希望这样做,请将 .skipping
设置为 insertPrevious
或 insertNext
的第一个参数。例如,考虑以下代码
let myElement = XElement("top") {
XElement("a")
}
for element in myElement.descendants {
if element.name == "a" {
element.insertNext() {
XElement("b")
}
}
else if element.name == "b" {
element.insertNext {
XElement("c")
}
}
}
myElement.echo(pretty: true)
输出
<top>
<a/>
<b/>
<c/>
</top>
当插入 <b/>
时,遍历也会跟踪此插入的内容。当您想跳过插入的内容时,请使用 .skipping
作为 insertNext
的第一个参数
...
element.insertNext(.skipping) {
XElement("b")
}
...
输出
<top>
<a/>
<b/>
</top>
类似地,如果您替换一个节点,则默认情况下,插入到节点位置的内容会包含在迭代中。示例:假设您想用其内容替换每个出现的 <bold>
元素
let document = try parseXML(fromText: """
<text><bold><bold>Hello</bold></bold></text>
""")
for bold in document.descendants("bold") { bold.replace { bold.content } }
document.echo()
输出为
<text>Hello</text>
后续的文本节点 (XText
) 始终会自动合并,并且具有空文本的文本节点会自动删除。相同的处理也适用于 XLiteral
节点。
当处理文本时,这可能非常方便,例如,然后可以直接将正则表达式应用于文档中的文本。但是,当文本节点和其他节点的不同行为影响您的操作结果时,可能会有一些绊脚石。
您可以通过将 isolated
属性设置为 true
来避免将文本 text
与其他文本合并(您也可以选择在 XText 的初始化期间设置此值)。考虑以下示例,其中搜索文本的出现会获得绿色的背景。在此示例中,您不希望在迭代中将 part
添加到 text
中
let searchText = "world"
document.traverse { node in
if let text = node as? XText {
if text.value.contains(searchText) {
text.isolated = true
var addSearchText = false
for part in text.value.components(separatedBy: searchText) {
text.insertPrevious {
addSearchText ? XElement("span", ["style": "background:LightGreen"]) {
searchText
} : nil
part
}
addSearchText = true
}
text.remove()
text.isolated = false
}
}
}
document.echo()
输出
<a>Hello <span style="background:LightGreen">world</span>, the <span style="background:LightGreen">world</span> is nice.</a>
请注意,例如在插入节点时,它们的 XText
节点在移动时被视为 isolated
。
在需要 XText
的地方可以使用 String
,例如,您可以编写 "Hello" as XText"
。
XText
以及 XLiteral
和 XCDATASection
都符合 XTextualContentRepresentation
协议,即它们都具有名为 value
的 String
属性,该属性可以读取和设置,并且表示将写入序列化文档的内容(在编写 XText
时需要一些字符转义)。请注意,XComment
不符合 XTextualContentRepresentation
协议。
当您只想对文档应用一些更改时,只需直接转到一些相应的元素并应用您想要的更改即可。但是,如果您想将整个文档转换为“其他内容”,则需要更好的工具来组织您对文档的操作,您需要“转换”。
正如一般描述中提到的,规则集 XRule
以 XTransformation
类型的转换实例的形式可以使用,如下所示。
在规则中,用户定义如何处理具有特定名称的元素或属性。然后可以将规则集应用于文档,即规则按照其定义顺序应用。这将重复进行,保证规则仅对同一对象应用一次(如果未从文档中删除并再次添加),直到没有应用发生为止。因此,可以在规则应用期间添加元素,然后稍后由相同或另一个规则处理。
示例
let document = try parseXML(fromText: """
<a><formula id="1"/></a>
""")
var count = 1
let transformation = XTransformation {
XRule(forElements: "formula") { element in
print("\n----- Rule for element \"formula\" -----\n")
print(" \(element)")
if count == 1 {
count += 1
print(" add image")
element.insertPrevious {
XElement("image", ["id": "\(count)"])
}
}
}
XRule(forElements: "image") { element in
print("\n----- Rule for element \"image\" -----\n")
print(" \(element)")
if count == 2 {
count += 1
print(" add formula")
element.insertPrevious {
XElement("formula", ["id": "\(count)"])
}
}
}
}
transformation.execute(inDocument: document)
print("\n----------------------------------------\n")
document.echo()
----- Rule for element "formula" -----
<formula id="1">
add image
----- Rule for element "image" -----
<image id="2">
add formula
----- Rule for element "formula" -----
<formula id="3">
----------------------------------------
<a><formula id="3"/><image id="2"/><formula id="1"/></a>
作为旁注,对于这样的 XTransformation
,元素名称的长度实际上并不重要:除了执行之前转换的初始化以及规则内部发生的事情之外,如果元素名称更长,规则的应用效率也不会降低。
您应该使用多个转换,每个转换都专用于一个单独的“主题”,而不是使用具有大量规则的转换。例如,对于某些文档格式,您可以先转换内联元素,然后再转换块元素。将转换拆分为多个转换实际上不会损害性能。
另请注意,规则的顺序很重要:如果您需要在规则中查找例如元素的父级,则了解此父级是否已被另一个规则更改很重要,即,先前的规则是否已转换此元素。以下部分“反向顺序的转换”中给出了一个示例。如前一段所述,使用多个转换可能对此有所帮助。在以下各节“带有上下文信息附件的转换”、“带有文档版本的转换”和“带有遍历的转换”中描述了使用更好上下文信息的方法。
另请注意,使用 XTransformation
您只能转换整个文档。在下面的“带有遍历的转换”部分中,描述了另一种转换任何 XML 树的选项。
可以通过在转换上调用 stop()
来停止转换,尽管这仅间接有效
var transformationAlias: XTransformation? = nil
let transformation = XTransformation {
XRule(forElements: "a") { _ in
transformationAlias?.stop()
}
}
transformationAlias = transformation
transformation.execute(inDocument: myDocument)
正如上一节中指出的那样,规则的顺序在某些转换中至关重要,例如,如果原始上下文很重要。
规则的“反向顺序”从内部元素到外部元素,以便在应用规则时上下文仍然不变,请注意查找 element.parent?.name
以区分文本的颜色
let document = try parseXML(fromText: """
<document>
<section>
<hint>
<paragraph>This is a hint.</paragraph>
</hint>
<warning>
<paragraph>This is a warning.</paragraph>
</warning>
</section>
</document>
""", textAllowedInElementWithName: { $0 == "paragraph" })
let transformation = XTransformation {
XRule(forElements: "paragraph") { element in
let style: String? = if element.parent?.name == "warning" {
"color:Red"
} else {
nil
}
element.replace {
XElement("p", ["style": style]) {
element.content
}
}
}
XRule(forElements: "hint", "warning") { element in
element.replace {
XElement("div") {
XElement("p", ["style": "bold"]) {
element.name.uppercased()
}
element.content
}
}
}
}
transformation.execute(inDocument: document)
document.echo(pretty: true)
结果
<document>
<section>
<div>
<p style="bold">HINT</p>
<p>This is a hint.</p>
</div>
<div>
<p style="bold">WARNING</p>
<p style="color:Red">This is a warning.</p>
</div>
</section>
</document>
此方法可能不完全适用于某些转换。
为了获取有关已转换元素在原始文档中的上下文信息,可以使用附件。请参阅以下代码中如何在构建 div
元素时使用 attached: ["source": element.name]
,以及如何在 paragraph
元素的规则中使用此信息(输入文档与上面“具有逆序的转换”部分中的文档相同;请注意,该部分中描述的逆序不在此处使用)。
let transformation = XTransformation {
XRule(forElements: "hint", "warning") { element in
element.replace {
XElement("div", attached: ["source": element.name]) {
XElement("p", ["style": "bold"]) {
element.name.uppercased()
}
element.content
}
}
}
XRule(forElements: "paragraph") { element in
let style: String? = if element.parent?.attached["source"] as? String == "warning" {
"color:Red"
} else {
nil
}
element.replace {
XElement("p", ["style": style]) {
element.content
}
}
}
}
transformation.execute(inDocument: document)
document.echo(pretty: true)
结果与上面“具有逆序的转换”部分中的结果相同。
如上面关于规则的部分所述,有时您需要了解已转换元素的原始上下文。为此,您可以使用文档版本,如下所述。
请注意,此方法会降低效率,因为需要创建一个(临时)克隆,但对于非常困难的转换,这可能会派上用场。当您需要以复杂的方式检查原始上下文时,可以使用此方法。
您首先创建一个文档版本(这会创建一个克隆,以便您当前的文档包含指向该克隆的反向链接),然后在某些规则中,您可以使用元素创建中的 withBackLinkFrom:
参数复制要替换的节点的反向链接(输入文档与上面“具有逆序的转换”部分中的文档相同)。
let transformation = XTransformation {
XRule(forElements: "hint", "warning") { element in
element.replace {
XElement("div", withBackLinkFrom: element) {
XElement("p", ["style": "bold"]) {
element.name.uppercased()
}
element.content
}
}
}
XRule(forElements: "paragraph") { element in
let style: String? = if element.parent?.backlink?.name == "warning" {
"color:Red"
} else {
nil
}
element.replace {
XElement("p", ["style": style]) {
element.content
}
}
}
}
// make a clone with inverse backlinks,
// pointing from the original document to the clone:
document.makeVersion()
transformation.execute(inDocument: document)
// remove the clone:
document.forgetLastVersion()
document.echo(pretty: true)
结果与上面“具有逆序的转换”部分中的结果相同。
还有另一种制定转换的可能性,它使用遍历,并且也可以应用于文档的某些部分或不属于文档的 XML 树。
由于 XML 树可以在遍历期间更改,因此您可以遍历 XML 树并在遍历期间更改树,例如,根据 switch
语句中当前元素的名称制定操作。
如果您在遍历的向下方向制定操作,您知道当前节点的父节点或其他祖先节点已被转换。相反,如果您仅在 up:
遍历部分中制定操作,并且从不操作当前元素的任何祖先,您知道父节点和其他祖先仍然是原始的(输入文档与上面“具有逆序的转换”部分中的文档相同)。
for section in document.elements("section") {
section.traverse { node in
// -
} up: { node in
if let element = node as? XElement {
guard node !== section else { return }
switch element.name {
case "paragraph":
let style: String? = if element.parent?.name == "warning" {
"color:Red"
} else {
nil
}
element.replace {
XElement("p", ["style": style]) {
element.content
}
}
case "hint", "warning":
element.replace {
XElement("div") {
XElement("p", ["style": "bold"]) {
element.name.uppercased()
}
element.content
}
}
default:
break
}
}
}
}
document.echo(pretty: true)
由于遍历的根节点在遍历期间不会被删除,因此有一个相应的 guard
语句。
结果与上面“具有逆序的转换”部分中的结果相同。
请注意,当使用遍历来转换 XML 树时,使用多个转换而不是一个转换会对效率产生负面影响。
当涉及到跟踪特定名称的元素并制定相应的规则时,该库非常强大。通过直接在这些点支持命名空间来添加额外的层将使库的实现更加复杂且效率更低。那么,让我们看看如何处理使用命名空间的 XML 文档。
首先,您始终可以在文档中查找命名空间前缀设置(属性 xmlns:...
)。正如在关于 XML 输入限制的部分中提到的,命名空间前缀通过 xmlns:...
属性的注释应仅位于 XML 源的根元素。然后,以下两个辅助方法可帮助您完成处理命名空间的任务。
从根元素读取命名空间 URL 字符串的完整前缀
XDocument.fullPrefix(forNamespace:) -> String
“完整”意味着会自动添加一个结束 :
。如果未定义前缀,则返回空字符串。
从根元素获取从命名空间 URL 字符串到完整前缀的映射
XDocument.fullPrefixesForNamespaces
当您想访问或更改该命名空间中的元素时,请在代码中动态添加相应的前缀
let fullMathMLPrefix = myDocument.fullPrefix(forNamespace: "http://www.w3.org/1998/Math/MathML")
let transformation = XTransformation {
XRule(forElements: "\(fullMathMLPrefix)a") { a in
...
}
...
如果您想在根元素中添加命名空间声明,请使用以下方法
XDocument.setNamespace(:withPossiblyFullPrefix:)
这里的前缀可能是“完整”前缀,即它可以包含一个结束 :
。对于同一命名空间但具有另一个前缀的现有命名空间声明不会 (!) 被删除。
请注意,这三个辅助方法也适用于元素。
您可以将 traverse
与使用 await
的闭包一起使用。您可以使用 Swift Async Algorithms package 的 async
属性(提供 AsyncLazySequence
)将 map
等应用于使用 await
的闭包(例如 element.children.async.map { await a.f($0) }
)。
目前,SwiftXML 包为使用 await
的闭包参数定义了 forEachAsync
方法,但如果 Swift Async Algorithms 包应为 AsyncLazySequence
定义此方法,则此方法可能会在包的未来版本中删除。
当以复杂方式处理 XML 时,XContent
具有以下非常方便的扩展
applying
:对实例应用一些更改并返回该实例pulling
:获取内容并返回其他内容,例如从中“拉出”某些内容fullfilling
:测试实例的条件,如果条件为真,则返回该实例,否则返回 nil
fullfills
:测试实例的条件并返回其结果(fullfilling
原则上是仅针对一项的 filter
方法的变体。)
很难用简单的示例来展示这些扩展的便利性,在简单的示例中,很容易在没有它们的情况下制定代码。但是,如果情况变得更加复杂,它们就会派上用场。
示例
let element1 = XElement("a") {
XElement("child-of-a") {
XElement("more", ["special": "yes"])
}
}
let element2 = XElement("b")
if let childOfA = element1.fullfilling({ $0.name == "a" })?.children.first,
childOfA.children.first?.fullfills({ $0["special"] == "yes" && $0["moved"] != "yes" }) == true {
element2.add {
childOfA.applying { $0["moved"] = "yes" }
}
}
element2.echo()
结果
<b><child-of-a moved="yes"><more special="yes"/></child-of-a></b>
applying
也为内容序列或元素序列预定义,在一般情况下(其中可能必须包含 return
语句)它比使用 map
方法更短,您可以直接使用它来定义内容(无需使用上面描述的 asContent
属性)
let myElement = XElement("a") {
XElement("b", ["inserted": "yes"]) {
XElement("c", ["inserted": "yes"])
}
}
print(Array(myElement.descendants.applying{ $0["inserted"] = "yes" }))
结果
[<b inserted="yes">, <c inserted="yes">]
public func copyXStructure(from start: XContent, to end: XContent, upTo: XElement? = nil, correction: ((StructureCopyInfo) -> XContent)?) -> XContent?
从 start
复制结构到 end
,可以选择复制到 upTo
值。start
和 end
必须有一个共同的祖先。如果没有共同的祖先,则返回 nil
。如果 a) upTo
不为 nil
且 b) upTo
是共同祖先或祖先本身的祖先,则返回的元素是 upTo
值的克隆。否则,它是共同祖先的克隆(但通常在两种情况下内容都不同)。correction
可以进行一些更正。
如果使用捆绑到 XTRansformation
中的 XRule
的多个实例来转换整个文档,则了解哪些属于哪些规则的动作“触及”了元素可能很有用。在调试版本中,转换执行期间执行的所有文件名和行号都记录在 encounteredActionsAt
属性中。