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“應用程式開發人員指南”)