跳轉到內容

XQuery/過濾節點

來自 Wikibooks,開放書籍,開放世界

您想建立過濾器,以移除或替換 XML 流中的特定節點。此流可能是記憶體中的 XML 文件,並且可能不在磁碟上。

要處理樹中的所有節點,我們將從稱為 身份轉換 的遞迴函式開始。此函式將源樹複製到輸出樹中,而不進行更改。我們從這個過程開始,然後為每個過濾器新增一些異常處理。

(: return a deep copy of  the element and all sub elements :)
declare function local:copy($element as element()) as element() {
   element {node-name($element)}
      {$element/@*,
          for $child in $element/node()
              return
               if ($child instance of element())
                 then local:copy($child)
                 else $child
      }
};

此函式使用稱為 **計算元素構造器** 的 XQuery 結構來構造元素。元素構造器的格式如下

  element {ELEMENT-NAME} {ELEMENT-VALUE}

在上述情況下,ELEMENT-VALUE 是另一個查詢,它查詢當前節點的所有子元素。for 迴圈選擇當前元素的所有節點,並執行以下虛擬碼

  if the child is another element ''(this uses the "instance of" instruction)''
      then copy the child ''(recursively)''
      else return the child ''(we have a leaf element of the tree)''

如果您理解此演算法的基本結構,您現在可以對其進行修改以僅過濾出所需的元素。您只需從這個模板開始,並修改各個部分。

請注意,您也可以使用 typeswitch 運算子來實現此函式

declare function local:copy($n as node()) as node() {
   typeswitch($n)
      case $e as element()
         return
            element {name($e)}
                    {$e/@*,
                     for $c in $e/(* | text())
                         return local:copy($c) }         
      default return $n
 };

移除所有屬性

[編輯 | 編輯原始碼]

以下函式從元素中移除所有屬性,因為不會複製屬性。

declare function local:copy-no-attributes($element as element()) as element() {
   element {node-name($element)}
      {
      for $child in $element/node()
         return
            if ($child instance of element())
               then local:copy-no-attributes($child)
               else $child
      }
};

此函式也可以透過使用 typeswitch 運算子來實現

declare function local:copy($n as node()) as node() {
   typeswitch($n)
      case $e as element()
         return
            element {name($e)}
                    {for $c in $e/(* | text())
                         return local:copy($c) }         
      default return $n
  };

該函式可以透過新增第二個函式引數來進行引數化,以指示應移除哪些屬性。

更改給定元素的所有屬性名

[編輯 | 編輯原始碼]
declare function local:change-attribute-name-for-element(
   $node as node(),
   $element as xs:string,
   $old-attribute as xs:string,
   $new-attribute as xs:string
   ) as element() {
       element
         {node-name($node)}
         {if (string(node-name($node))=$element)
           then
              for $att in $node/@*
              return
                if (name($att)=$old-attribute)
                  then
                     attribute {$new-attribute} {$att}
                   else
                      attribute {name($att)} {$att}
           else
              $node/@*
           ,
               for $child in $node/node()
                 return if ($child instance of element())
                    then local:change-attribute-name-for-element($child, $element, $old-attribute, $new-attribute)
                    else $child 
         }
};

替換所有屬性值

[編輯 | 編輯原始碼]

對於具有特定屬性名的所有元素,將舊屬性值替換為新的屬性值。

declare function local:change-attribute-values
    (
        $node as node(),
        $element-name as xs:string*,
        $attribute-name as xs:string*,
        $old-attribute-value as xs:string*,
        $new-attribute-value as xs:string
    )
        as element() 
    {

        element{node-name($node)}
        {
        if (string(node-name($node))=$element-name)
        then
           for $attribute in $node/@*
               let $found-attribute-name := name($attribute)
               let $found-attribute-value := string($attribute)
                   return
                       if ($found-attribute-name = $attribute-name and $found-attribute-value = $old-attribute-value)
                       then attribute {$found-attribute-name} {$new-attribute-value}
                       else attribute {$found-attribute-name} {$found-attribute-value}
        else $node/@*
        ,
        for $node in $node/node()
           return 
               if ($node instance of element())
               then local:change-attribute-values($node, $element-name, $attribute-name, $old-attribute-value, $new-attribute-value)
               else $node 
    }
};

移除命名屬性

[編輯 | 編輯原始碼]

屬性在謂詞表達式 **not(name()=$attribute-name)** 中被過濾,以便省略命名屬性。

declare function local:copy-filter-attributes(
       $element as element(),
       $attribute-name as xs:string*) as element() {
    element {node-name($element)}
            {$element/@*[not(name()=$attribute-name)],
                for $child in $element/node()
                   return if ($child instance of element())
                      then local:copy-filter-attributes($child, $attribute-name)
                      else $child
            }
  };

移除命名元素

[編輯 | 編輯原始碼]

同樣,元素可以在謂詞中過濾

declare function local:remove-elements($input as element(), $remove-names as xs:string*) as element() {
   element {node-name($input) }
      {$input/@*,
       for $child in $input/node()[not(name(.)=$remove-names)]
          return
             if ($child instance of element())
                then local:remove-elements($child, $remove-names)
                else $child
      }
};

這在謂詞中添加了 node() 限定符和節點的名稱

/node()[not(name(.)=$element-name)]

要使用此函式,只需將輸入 XML 作為第一個引數,並將元素名稱序列作為字串作為第二個引數傳遞。例如

  let $input := doc('my-input.xml')
  let $remove-list := ('xxx', 'yyy', 'zzz')
  local:remove-elements($input,  $remove-list)

使用對映重新命名元素

[編輯 | 編輯原始碼]

假設我們有一個元素檔案,我們希望使用過濾器對其進行重新命名。我們希望將重新命名規則儲存在這樣的檔案中

let $rename-map :=
<rename-map>
   <map>
      <from>b</from>
      <to>x</to>
   </map>
   <map>
      <from>d</from>
      <to>y</to>
   </map>
   <map>
      <from>f</from>
      <to>z</to>
   </map>
</rename-map>

重新命名元素函式如下

declare function local:rename-elements($input as node(), $map as node()) as node() {
let $current-element-name := name($input)
return
   (: we create a new element with a name and a content :)
   element
        { (: the new name is created here :)
        if (local:element-in-map($current-element-name, $map)  ) 
           then local:new-name($current-element-name, $map)
           else node-name($input)
        }
        { (: the element content is created here :)
        $input/@*, (: copy all attributes :)
        for $child in $input/node()
         return
            if ($child instance of element())
               then local:rename-elements($child, $map)
               else $child
        }
};

(: return true() if an element is in the form of a rename map :)
declare function local:element-in-map($element-name as xs:string, $map as node()) as xs:boolean {
exists($map/map[./from = $element-name])
};

(: return the new element name of an element in a rename map :)
declare function local:new-name($element-name as xs:string, $map as node()) as xs:string {
$map/map[./from = $element-name]/to
};

以下是輸入和輸出

<data>
   <a q="joe">a</a>
   <b p="5" q="fred" >bb</b>
   <c>
        <d>dd</d>
         <a q="dave">aa</a>
         <e>EE</e>
         <f>FF</f>
   </c>
</data>
<data>
   <a q="joe">a</a>
   <x q="fred" p="5">bb</x>
   <c>
            <y>dd</y>
            <a q="dave">aa</a>
            <e>EE</e>
            <z>FF</z>
   </c>
</data>

移除空元素

[編輯 | 編輯原始碼]

許多 RDBMS 系統匯出轉換為 XML 的資料行。最佳做法是移除任何沒有文字內容的 XML 元素。

XQuery 函式將接收單個元素,並返回可選元素。

第一個測試是檢查是否存在子元素或文字。如果存在,則構造一個元素,並新增屬性。然後,對於每個子元素,該函式呼叫自身。

declare function local:remove-empty-elements($element as element()) as element()? {
if ($element/* or $element/text())
  then 
   element {node-name($element)}
      {$element/@*,
          for $child in $element/node()
              return
               if ($child instance of element())
                 then local:remove-empty-elements($child)
                 else $child
      }
    else ()
};

示例輸入

let $input :=
<root>
   <a>A</a>
   <!-- remove these -->
   <b></b>
   <c> </c>
   <d>
      <e>E</e>
      <!-- and this -->
      <f>   </f>
   </d>
</root>

示例輸出

<root>
   <a>A</a>
   <!-- remove these -->
   <d>
      <e>E</e>
      <!-- and this -->
   </d>
</root>

請注意,即使元素包含空格、回車符或製表符,元素也會被移除。

示例說明上述過濾器

[編輯 | 編輯原始碼]

以下指令碼演示了這些函式

let $x :=
<data>
   <a q="joe">a</a>
   <b p="5" q="fred" >bb</b>
   <c>
        <d>dd</d>
         <a q="dave">aa</a>
   </c>
</data>
return
 <output>
    <original>{$x}</original>
    <fullcopy> {local:copy($x)}</fullcopy>
    <noattributes>{local:copy-no-attributes($x)}  </noattributes>
    <filterattributes>{local:copy-filter-attributes($x,"q")}</filterattributes>
    <filterelements>{local:copy-filter-elements($x,"a")}</filterelements>
    <filterelements2>{local:copy-filter-elements($x,("a","d"))}  </filterelements2>
 </output>

執行

轉換為 XHTML 名稱空間

[編輯 | 編輯原始碼]
declare function local:xhtml-namespace($nodes as node()*) as node()* {
for $node in $nodes
   return
    if ($node instance of element())
      then
         element {QName('http://www.w3.org/1999/xhtml', local-name($node))}
            {$node/@*, local:xhtml-namespace($node/node())}
      else $node
 };

新增名稱空間

[編輯 | 編輯原始碼]

這是一個函式,它將名稱空間新增到 XML 文件中的根元素。請注意,它使用具有兩個引數的元素構造器。第一個引數是元素名稱,第二個引數是元素內容。元素名稱使用 QName() 函式建立,元素內容使用 {$in/@*, $in/node()} 建立,它將新增屬性和所有子節點。

declare function local:change-root-namespace($in as element()*, $new-namespace as xs:string, $prefix as xs:string) as element()? {
for $element in $in
   return
     element {QName($new-namespace,
         concat($prefix,
                if ($prefix = '')
                   then '' else ':',
                local-name($in)))}
           {$in/@*, $in/node()}
 };

如果我們使用以下輸入

 let $input :=
<a>
  <b>
     <c a1="A1" a2="A2">
       <d a1="A1" a2="A2">DDD</d>
     </c>
  </b>
  <e>EEE</e>
</a>
return local:change-root-namespace($input, 'http://example.com', 'e')
(: $in is a sequence of nodes! :)
declare function local:change-namespace-deep($in as node()*, $new-namespace as xs:string, $prefix as xs:string )  as node()* {
  for $node in $in
  return if ($node instance of element())
         then element
               {QName ($new-namespace,
                          concat($prefix,
                                if ($prefix = '')
                                then '' else ':',
                                local-name($node)))
               }
               {$node/@*, local:change-namespace-deep($node/node(), $new-namespace, $prefix)}
         else
            (: step through document nodes :)
            if ($node instance of document-node())
               then local:change-namespace-deep($node/node(), $new-namespace, $prefix)
               (: for comments and PIs :)
               else $node
 };
 
let $input :=
<a>
  <b>
     <c a1="A1" a2="A2">
       <!-- comment -->
       <d a1="A1" a2="A2">DDD</d>
     </c>
  </b>
  <e>EEE</e>
</a>

return local:change-namespace-deep($input, 'http://example.com', '')

該函式將返回以下輸出

<e:a xmlns:e="http://example.com">
   <b>
      <c a1="A1" a2="A2">
         <d a1="A1" a2="A2">DDD</d>
      </c>
   </b>
   <e>EEE</e>
</e:a>
<a xmlns="http://example.com">
   <b>
      <c a1="A1" a2="A2"><!-- comment -->
         <d a1="A1" a2="A2">DDD</d>
      </c>
   </b>
   <e>EEE</e>
</a>

請注意,如果您使用 null 作為字首,則字首不會在根元素中使用。但是,名稱空間將被使用。

移除不需要的名稱空間

[編輯 | 編輯原始碼]

一些系統不允許您在進行更新後精確控制使用的名稱空間,即使使用了 copy-namespaces 宣告。

移除 TEI 名稱空間

[編輯 | 編輯原始碼]

以下 XQuery 函式是一個示例,它將從節點中移除 TEI 名稱空間。

declare function local:clean-namespaces($node as node()) {
    typeswitch ($node)
        case element() return
            if (namespace-uri($node) eq "http://www.tei-c.org/ns/1.0") then
                element { QName("http://www.tei-c.org/ns/1.0", local-name($node)) } {
                    $node/@*, for $child in $node/node() return local:clean-namespaces($child)
                }
            else
                $node
        default return
            $node
};

以下兩個函式將從節點中移除任何名稱空間,nnsc 代表無名稱空間複製。第一個執行速度快得多:從我有限的理解來看,它跳過屬性的速度更快。另一個仍然存在,可能隱藏了一些棘手的技巧。

移除所有名稱空間

[編輯 | 編輯原始碼]

以下遞迴函式將從元素和屬性中移除所有名稱空間。請注意,local-name() 函式用於生成無名稱空間的屬性名稱。

(: return a deep copy of the elements and attributes without ANY namespaces :)
declare function local:remove-namespaces($element as element()) as element() {
     element { local-name($element) } {
         for $att in $element/@*
         return
             attribute {local-name($att)} {$att},
         for $child in $element/node()
         return
             if ($child instance of element())
             then local:remove-namespaces($child)
             else $child
         }
};

版本 2

此版本使用 @* 和 * 生成屬性和元素的單個序列。元素傳遞遞迴函式,但屬性直接作為 $child 返回。

(: return a deep copy of the element with out namespaces :)
declare function local:nnsc2($element as element()) as element() {
     element { QName((), local-name($element)) } {
         for $child in $element/(@*,*)
         return
             if ($child instance of element())
             then local:nnsc2($child)
             else $child
     }
};

相反,如果您想將名稱空間新增到元素,Misztur、Chrisblog 帖子中的一個起點是:http://fgeorges.blogspot.com/2006/08/add-namespace-node-to-element-in.html

移除多餘的空格

[編輯 | 編輯原始碼]
declare function forxml:sanitize($forxml-result)
{
   let $children := $forxml-result/*
   return
       if(empty($children)) then ()
       else
           for $c in $children
           return
           (
               element { name($c) }
               {
                    $c/@*,
                    if(functx:is-a-number($c/text()))
                    then number($c/text())
                    else normalize-space($c/text()),
                    forxml:sanitize($c)
               }
            )
};

由 Chris Misztur 貢獻。

移除沒有字串值的元素

[編輯 | 編輯原始碼]

不包含字串值或僅包含空格的元素可以被移除。

declare function local:remove-empty-elements($nodes as node()*)  as node()* {
   for $node in $nodes
   return
     if ($node instance of element())
     then if (normalize-space($node) = '')
          then ()
          else element { node-name($node)}
                { $node/@*,
                  local:remove-empty-elements($node/node())}
     else if ($node instance of document-node())
     then local:remove-empty-elements($node/node())
     else $node
 } ;

移除空屬性

[編輯 | 編輯原始碼]

不包含文字的屬性可以被去除。

declare function local:remove-empty-attributes($element as element()) as element() {
element { node-name($element)}
{ $element/@*[string-length(.) ne 0],
for $child in $element/node( )
return 
    if ($child instance of element())
    then local:remove-empty-attributes($child)
    else $child }
};

一個函式用於多個記憶體操作

[編輯 | 編輯原始碼]

你可以將多個用於改變節點樹的函式整合到一個函式中。在下面的函式中,可以方便地執行一些常見的元素操作。

傳遞的引數是 1) 要操作的節點樹,2) 要插入的任何新專案,3) 要執行的操作,4) 操作所針對的元素名稱。

該函式可以將一個或多個作為引數提供的元素插入到節點樹中目標元素的特定位置(在之前、之後或作為第一個或最後一個子元素)。

一個或多個元素可以插入到與目標元素相同的位置,即可以替代它們。

如果操作是 'remove',則移除目標元素。如果操作是 'remove-if-empty',則如果目標元素沒有(規範化的)字串值,則移除它們。如果操作是 'substitute-children-for-parent',則用它們的子元素替代目標元素。(在最後三種情況下,不考慮新內容引數,為了清楚起見,它應該是空序列)。

如果要執行的操作是 'change-name',則元素名稱將更改為新內容的第一個專案。

如果要執行的操作是 'substitute-content',則目標元素的任何子元素將被新內容替換。

請注意,可以將無上下文函式(例如 current-date())作為新內容傳遞。

declare function local:change-elements($node as node(), $new-content as item()*, $action as xs:string, $target-element-names as xs:string+) as node()+ {
        
        if ($node instance of element() and local-name($node) = $target-element-names)
        then
            if ($action eq 'insert-before')
            then ($new-content, $node) 
            else
            
            if ($action eq 'insert-after')
            then ($node, $new-content)
            else
            
            if ($action eq 'insert-as-first-child')
            then element {node-name($node)}
                {
                $node/@*
                ,
                $new-content
                ,
                for $child in $node/node()
                    return $child
                }
                else
            
            if ($action eq 'insert-as-last-child')
            then element {node-name($node)}
                {
                $node/@*
                ,
                for $child in $node/node()
                    return $child 
                ,
                $new-content
                }
                else
                
            if ($action eq 'substitute')
            then $new-content
            else 
                
            if ($action eq 'remove')
            then ()
            else 
                
            if ($action eq 'remove-if-empty')
            then
                if (normalize-space($node) eq '')
                then ()
                else $node
            else

            if ($action eq 'substitute-children-for-parent')
            then $node/*
            else
            
            if ($action eq 'substitute-content')
            then
                element {name($node)}
                    {$node/@*,
                $new-content}
            else
                
            if ($action eq 'change-name')
            then
                element {$new-content[1]}
                    {$node/@*,
                for $child in $node/node()
                    return $child}
                
            else ()
        
        else
        
            if ($node instance of element()) 
            then
                element {node-name($node)} 
                {
                    $node/@*
                    ,
                    for $child in $node/node()
                        return 
                            local:change-elements($child, $new-content, $action, $target-element-names) 
                }
            else $node
};

不使用型別切換,因為它需要靜態引數。

具有以下主體,

let $input := 
<html>
    <head n="1">1</head>
    <body>
        <p n="2">2</p>
        <p n="3">3</p>
    </body>
</html>

let $new-content := <p n="4">0</p>

return 
    local:change-elements($input, $new-content, 'insert-as-last-child', ('body'))

結果將是

<html>
    <head n="1">1</head>
    <body>
        <p n="2">2</p>
        <p n="3">3</p>
        <p n="4">0</p>
    </body>
</html>

請注意,如果目標元素是 'p',則 $new-node 將相對於名為 'p' 的每個元素插入。

你可以填寫任何需要的函式,並刪除不需要的函式。

以下函式簡化了對屬性的多個操作。這比處理元素更復雜,因為元素名稱必須被考慮在內。

傳遞的引數是 1) 要操作的節點樹,2) 新屬性名稱,3) 新屬性內容,4) 要執行的操作,5) 操作所針對的元素名稱,6) 操作所針對的屬性名稱。

只需使用 action 引數,就可以移除所有空屬性。

如果你想要移除所有名為屬性,你需要提供要移除的屬性名稱。

如果你想要更改所有名為屬性的值,你需要提供新的值。

如果你想要將一個屬性(帶有名稱和值)附加到一個特定元素,你需要提供要附加屬性的元素引數、屬性名稱和屬性值,以及操作。

如果你想要從一個特定元素中移除一個屬性,你需要提供要移除屬性的元素引數、屬性名稱,以及操作。

如果你想要更改附加到特定元素的屬性名稱,你需要提供屬性附加到的元素引數、屬性現有的名稱、要更改的新屬性名稱,以及操作。

declare function local:change-attributes($node as node(), $new-name as xs:string, $new-content as item(), $action as xs:string, $target-element-names as xs:string+, $target-attribute-names as xs:string+) as node()+ {
    
            if ($node instance of element()) 
            then
                element {node-name($node)} 
                {
                    if ($action = 'remove-all-empty-attributes')
                    then $node/@*[string-length(.) ne 0]
                    else 
                        
                    if ($action = 'remove-all-named-attributes')
                    then $node/@*[name(.) != $target-attribute-names]
                    else 
                    
                    if ($action = 'change-all-values-of-named-attributes')
                    then element {node-name($node)}
                    {for $att in $node/@*
                        return 
                            if (name($att) = $target-attribute-names)
                            then attribute {name($att)} {$new-content}
                            else attribute {name($att)} {$att}
                    }
                    else
                        
                    if ($action = 'attach-attribute-to-element' and name($node) = $target-element-names)
                    then ($node/@*, attribute {$new-name} {$new-content})
                    else 

                    if ($action = 'remove-attribute-from-element' and name($node) = $target-element-names)
                    then $node/@*[name(.) != $target-attribute-names]
                    else 

                    if ($action = 'change-attribute-name-on-element' and name($node) = $target-element-names)
                    then 
                        for $att in $node/@*
                            return
                                if (name($att) = $target-attribute-names)
                                then attribute {$new-name} {$att}
                                else attribute {name($att)} {$att}
                    else
                    
                    if ($action = 'change-attribute-value-on-element' and name($node) = $target-element-names)
                    then
                        for $att in $node/@*
                            return 
                                if (name($att) = $target-attribute-names)
                                then attribute {name($att)} {$new-content}
                                else attribute {name($att)} {$att}
                    else 

                    $node/@*
                    ,
                    for $child in $node/node()
                        return 
                            local:change-attributes($child, $new-name, $new-content, $action, $target-element-names, $target-attribute-names) 
                }
            else $node
};

具有以下主體,

let $input := 
<xml>
    <head n="1">1</head>
    <body>
        <p n="2">2</p>
        <p n="3">3</p>
    </body>
</xml>

return 
    local:change-attributes($input, 'y', current-date(), 'change-attribute-value-on-element', 'p', 'n')

結果將是

<xml>
    <head n="1" x="">1</head>
    <body>
        <p n="2013-11-30+01:00">2</p>
        <p n="2013-11-30+01:00">3</p>
    </body>
</xml>

參考文獻

[編輯 | 編輯原始碼]

W3C 關於計算元素構造器的頁面

華夏公益教科書