跳转到内容

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 关于计算元素构造器的页面

华夏公益教科书