XQuery/Typeswitch 变换
您有一个 XML 文档,您希望将其转换为另一种格式的 XML。您希望控制和定制转换过程,并且希望以模块化方式存储转换规则,以便您或其他人可以轻松地修改和维护它们。
您可能听说过这样的传统智慧:“XQuery 最适合查询或选择 XML,而 XSLT 最适合转换 XML”。实际上,两种方法都能够转换 XML。尽管 XSLT 历史稍长,安装基础更大,但“XQuery typeswitch”转换 XML 的方法提供了许多优势。这些优势在 XQuery 优势 中有更详细的介绍。
我们将使用 XQuery 的 typeswitch 表达式将 XML 文档从一种形式转换为另一种形式。基本方法简单明了:对于输入文档中的每个 XML 节点,我们将指定在输出文档中应该创建什么。typeswitch 表达式执行此核心功能,即识别源文档中每个节点发生的情况。我们将编写一个 XQuery 函数,该函数接收一个节点,使用 typeswitch 表达式对其进行测试,并将该节点 **分派** 到相应的处理函数,该函数将节点转换为新格式并将所有子元素使用 **passthru** 函数发送回主函数。这种递归例程有效地遍历整个节点及其子节点,将它们转换为目标格式。一旦结构设置完毕,变换就很容易修改,即使输入文档中的标签嵌套非常复杂。(尾递归技术对于 XSLT 的精明用户来说将是熟悉的,但本文绝对没有 XSLT 先决条件。)
假设您有一个简单的 XML 文档,您希望对其进行转换
<bill>
<!-- This is a XML comment -->
<btitle>This is the Bill title</btitle>
<section-id>1</section-id>
<bill-text>This is the text with <s>many</s> examples.</bill-text>
</bill>
以下是您希望将源输入转换为的格式
<Bill>
<!-- This is a XML comment -->
<BillTitleText>This is the Bill title</BillTitleText>
<BillSectionID>1</BillSectionID>
<BillText>This is the text with <del>many</del> examples.</BillText>
</Bill>
在创建 typeswitch 变换时,有两个重要的选项。一个选择是您是使用单个 node() 参数,还是使用节点序列作为参数。
第二个重要选项是您希望默认操作是什么。可以将默认值配置为传递或删除所有未匹配 typeswitch 语句的元素。
使用 typeswitch 表达式转换 XML 的最有效方法是创建一系列 XQuery 函数。通过这种方式,我们可以将变换的主要操作干净地分离到模块化函数中。(实际上,函数库可以保存到 XQuery 库模块中,然后可以被其他 XQuery 重用。)这种 typeswitch 样式变换的“魔力”在于,一旦您理解了函数的基本模式和结构,就可以将其适应自己的数据。您会发现这种结构非常模块化且直观,甚至可以在短时间内教其他人这种模式的基础知识,并赋予他们自己维护和更新变换规则的能力。
模块中的第一个函数是 typeswitch 表达式所在的位置。此函数通常称为“分派”函数
declare function local:dispatch($node as node()) as item()* {
typeswitch($node)
case text() return $node
case comment() return $node
case element(bill) return local:bill($node)
case element(btitle) return local:btitle($node)
case element(section-id) return local:section-id($node)
case element(bill-text) return local:bill-text($node)
case element(strike) return local:strike($node)
default return local:passthru($node)
};
请注意,typeswitch 表达式根据一系列条件测试输入节点:该节点是文本节点、注释节点、bill 元素、betitle 元素、section-id 元素等?如果是文本节点(例如“这是法案标题”),我们只需返回文本,不做任何修改。(请注意,text() 节点测试排在首位,因为 text() 可能是文本丰富的文档中最丰富的节点类型,将最常见的类型放在首位可以提高性能。)如果该节点是 bill 元素,那么我们将节点传递给名为 local:bill() 的函数以进行特定于 bill 的处理。local:bill() 函数(见下文)将 <bill> 元素转换为 <Bill> 元素。然后它将 bill 元素的内容传递给 local:passthru() 函数。如果我们的节点与任何预定义规则都不匹配,那么 typeswitch 表达式将使用必需的最终“默认值”(想想:“回退”)语句;此默认值用于与所有前面测试不匹配的节点。在我们的示例中,默认表达式将不匹配的节点发送到 local:passthru() 函数。(Typeswitch 不限于匹配 text() 和 element() 节点;它还可以匹配其他节点类型:processor-instruction() 和 comment(),但通常不匹配 attribute()。属性通常在属性父元素的处理函数中处理,而不是在核心 typeswitch 函数中处理。)
passthru() 函数递归遍历给定节点的子节点,将它们中的每一个都返回给主 typeswitch 操作。
declare function local:passthru($nodes as node()*) as item()* {
for $node in $nodes/node() return local:dispatch($node)
};
(*注意:这个函数非常简单,看起来可能多余。为什么不简单地用 local:dispatch($node/node()) 替换 local:passthru($node) 的实例?它主要的好处是简化了代码,免除了您为每次递归键入额外的 "/node()" 的负担。第二个好处是它引入了在节点发送到 typeswitch 例程之前对其进行过滤的可能性。)
上面的 local:passthru() 函数会从您的节点中删除所有属性。如果您在输入 XML 中有要保留的属性,请使用以下 passthru() 函数作为替代。
declare function local:passthru($node as node()*) as item()* {
element {name($node)} {($node/@*, local:dispatch($node/node()))}
};
declare function local:bill($node as element(bill)) as element() {
<Bill>{local:passthru($node)}</Bill>
};
declare function local:btitle($node as element(btitle)) as element() {
<BillTitle>{local:passthru($node)}</BillTitle>
};
declare function local:section-id($node as element(section-id)) as element() {
<BillSectionID>{local:passthru($node)}</BillSectionID>
};
declare function local:strike($node as element(strike)) as element() {
<del>{local:passthru($node)}</del>
};
declare function local:bill-text($node as element(bill-text)) as element() {
<BillText>{local:passthru($node)}</BillText>
};
现在,我们可以编写一个查询,该查询接收源 XML 并使用 local:dispatch() 函数将输入转换为目标格式。
let $input :=
<bill>
<!-- This is a XML comment -->
<btitle>This is the Bill title</btitle>
<section-id>1</section-id>
<bill-text>This is the text with <s>many</s> examples.</bill-text>
</bill>
return
local:dispatch($input)
虽然上述方法被推荐为最模块化、可扩展的方法,但使用更紧凑、自包含的函数来表达相同的转换是完全可以接受的。
declare function local:transform($nodes as node()*) as item()* {
for $node in $nodes
return
typeswitch($node)
case text() return $node
case comment() return $node
case element(bill) return element Bill {local:transform($node/node())}
case element(btitle) return element BillTitle {local:transform($node/node())}
case element(section-id) return element BillSectionID {local:transform($node/node())}
case element(strike) return element del {local:transform($node/node())}
case element(bill-text) return element BillText {local:transform($node/node())}
default return local:transform($node/node())
};
除了该函数完全自包含(以 FLWOR 表达式开头,并使用 $node/node() 递归遍历子节点)之外,请注意该函数使用计算元素构造器来完成转换。
这是 XQuery Typeswitch 方法进行 XML 文档转换的核心。基于这种简单模式,已经编写了整个库来将 TEI、DocBook 和 Office OpenXML 文档等源格式转换为 XHTML、XSL-FO 和彼此之间的其他格式。
虽然我们可以手动创建 typeswitch 模块,逐个元素地构建它们,但我们也可以使用 XQuery 生成一个骨架 typeswitch 模块;请参阅本文的配套文章 XQuery/Generating_Skeleton_Typeswitch_Transformation_Modules。除了“骨架生成器”之外,本文还提供了使用 XQuery typeswitch 的更复杂转换模式的示例:更改元素的名称、忽略元素、根据元素的上下文进行不同转换、重新排序元素。它还提供了 XQuery 和 XSLT 对相同示例转换方法的详细比较,因此对于来自 XSLT 世界的读者来说很有用。
- DocBook 到 XHTML Dan McCreary 的 eXist 分支中将 Docbook 转换为 XHTML 的示例代码链接
- W3C XQuery Typeswitch 定义
- typeswitch 和 XSLT apply-templates 的比较
- Ryan Semerau 的 i18n 示例
- BEA/Oracle mapper 中的 typeswitch
- 2002 年 12 月 Per Bothner 关于使用 typeswitch 将 XML 转换为 HTML 的 xml.com 文章
- 使用递归 typeswitch 表达式转换 XML 结构(来自 MarkLogic“应用程序开发人员指南”)