【问题标题】:Xmlstarlet - copy value of one attribute to another, if it exists in an elementXmlstarlet - 将一个属性的值复制到另一个,如果它存在于一个元素中
【发布时间】:2022-12-18 20:46:31
【问题描述】:

我刚刚了解了 xmlstarlet,但不幸的是我在 XML 方面遇到了很多困难,所以我希望我能得到一些帮助......

比如说,我有这个 XML 文件,test.xml

<?xml version="1.0" encoding="UTF-8"?>
<objects>
  <g id="layer3" inkscape:label="hello">
    <circle id="circ2" inkscape:label="there"/>
    <rect id="rect2" inkscape:label="world"/>
  </g>
  <g id="layer4">
    <circle id="circ3" inkscape:label="more"/>
  </g>
</objects>

所以我想做的是:对于存在inkscape:label属性的每个节点,将inkscape:label属性的值复制到id属性;所以上面的预期输出是:

<?xml version="1.0" encoding="UTF-8"?>
<objects>
  <g id="hello" inkscape:label="hello">
    <circle id="there" inkscape:label="there"/>
    <rect id="world" inkscape:label="world"/>
  </g>
  <g id="layer4">
    <circle id="more" inkscape:label="more"/>
  </g>
</objects>

我怎样才能用 xmlstarlet 做到这一点?


显然,我可以使用表达式 string("TEST") 将所有 id 属性替换为固定值,如下所示:

$  xmlstarlet edit -N inkscape="http://www.inkscape.org/namespaces/inkscape" --update '//*/@id' --expr 'string("TEST")'
test.xml
test.xml:3.40: Namespace prefix inkscape for label on g is not defined
  <g id="layer3" inkscape:label="hello">
                                       ^
test.xml:4.46: Namespace prefix inkscape for label on circle is not defined
    <circle id="circ2" inkscape:label="there"/>
                                             ^
test.xml:5.44: Namespace prefix inkscape for label on rect is not defined
    <rect id="rect2" inkscape:label="world"/>
                                           ^
test.xml:8.45: Namespace prefix inkscape for label on circle is not defined
    <circle id="circ3" inkscape:label="more"/>
                                            ^
<?xml version="1.0" encoding="UTF-8"?>
<objects>
  <g id="TEST" inkscape:label="hello">
    <circle id="TEST" inkscape:label="there"/>
    <rect id="TEST" inkscape:label="world"/>
  </g>
  <g id="TEST">
    <circle id="TEST" inkscape:label="more"/>
  </g>
</objects>

...我可以像这样用表达式string(../@id)“重新插入”属性 id 的值(所以我基​​本上得到与输入相同的输出):

$ xmlstarlet edit -N inkscape="http://www.inkscape.org/namespaces/inkscape" --update '//*/@id' --expr 'string(../@id)' test.xml
test.xml:3.40: Namespace prefix inkscape for label on g is not defined
  <g id="layer3" inkscape:label="hello">
                                       ^
test.xml:4.46: Namespace prefix inkscape for label on circle is not defined
    <circle id="circ2" inkscape:label="there"/>
                                             ^
test.xml:5.44: Namespace prefix inkscape for label on rect is not defined
    <rect id="rect2" inkscape:label="world"/>
                                           ^
test.xml:8.45: Namespace prefix inkscape for label on circle is not defined
    <circle id="circ3" inkscape:label="more"/>
                                            ^
<?xml version="1.0" encoding="UTF-8"?>
<objects>
  <g id="layer3" inkscape:label="hello">
    <circle id="circ2" inkscape:label="there"/>
    <rect id="rect2" inkscape:label="world"/>
  </g>
  <g id="layer4">
    <circle id="circ3" inkscape:label="more"/>
  </g>
</objects>

...但我不能使用相同的技巧(表达式string(../@inkscape:label) - 或string(../@*[local-name()='label']) 按照How does local-name find attributes in an xml node?)从属性inkscape:label 读取 - 我无法真正判断这是否是因为“命名空间前缀”。 .“未定义”消息:

$ xmlstarlet edit -N inkscape="http://www.inkscape.org/namespaces/inkscape" --update '//*/@id' --expr 'string(../@inkscape:label)' test.xml
test.xml:3.40: Namespace prefix inkscape for label on g is not defined
  <g id="layer3" inkscape:label="hello">
                                       ^
test.xml:4.46: Namespace prefix inkscape for label on circle is not defined
    <circle id="circ2" inkscape:label="there"/>
                                             ^
test.xml:5.44: Namespace prefix inkscape for label on rect is not defined
    <rect id="rect2" inkscape:label="world"/>
                                           ^
test.xml:8.45: Namespace prefix inkscape for label on circle is not defined
    <circle id="circ3" inkscape:label="more"/>
                                            ^
<?xml version="1.0" encoding="UTF-8"?>
<objects>
  <g id="" inkscape:label="hello">
    <circle id="" inkscape:label="there"/>
    <rect id="" inkscape:label="world"/>
  </g>
  <g id="">
    <circle id="" inkscape:label="more"/>
  </g>
</objects>

通过get attribute value using xmlstarlet or xmllint;我可以确认我可以使用以下方法定位 id 属性:

xmlstarlet select -N inkscape="http://www.inkscape.org/namespaces/inkscape" --template --value-of '//*/@id' test.xml

...但是 inkscape:label 的相应命令不返回任何内容:

xmlstarlet select -N inkscape="http://www.inkscape.org/namespaces/inkscape" --template --value-of '//*/@inkscape:label' test.xml

这可能是命名空间的事情,但我不明白我怎么能忽略命名空间,而只与文档中的属性名称相关......


编辑:终于用 Python 3 解决了这里的问题:

#!/usr/bin/env python3

# https://stackoverflow.com/questions/30097949/elementtree-findall-to-recursively-select-all-child-elements
# https://stackoverflow.com/questions/13372604/python-elementtree-parsing-unbound-prefix-error
# https://stackoverflow.com/questions/2352840/parsing-broken-xml-with-lxml-etree-iterparse
# https://stackoverflow.com/questions/28813876/how-do-i-get-pythons-elementtree-to-pretty-print-to-an-xml-file

import sys
import lxml
import lxml.etree
import xml.etree.ElementTree as ET

def proc_node(node):
  target_label = 'inkscape:label' # file without namespace, like `test.xml` here
  #target_label = '{http://www.inkscape.org/namespaces/inkscape}label' # file with namespace (like proper Inkscape .svg)
  if target_label in node.attrib:
    node.attrib['id'] = node.attrib[target_label]
  for childel in node.getchildren():
    proc_node(childel)


parser1 = lxml.etree.XMLParser(encoding="utf-8", recover=True)
tree1 = ET.parse('test.xml', parser1)
ET.indent(tree1, space="  ", level=0)
proc_node(tree1.getroot())
print(lxml.etree.tostring(tree1.getroot(), xml_declaration=True, pretty_print=True, encoding='UTF-8').decode('utf-8'))

...如果我调用这个xmlproc.py,那么结果是:

$ python3 xmlproc.py
<?xml version='1.0' encoding='UTF-8'?>
<objects>
  <g id="hello" inkscape:label="hello">
    <circle id="there" inkscape:label="there"/>
    <rect id="world" inkscape:label="world"/>
  </g>
  <g id="layer4">
    <circle id="more" inkscape:label="more"/>
  </g>
</objects>

...这正是我想要的。

因此,本着如何假设问题的精神来指定 - 我如何使用 xmlstarlet 实现这一点?

【问题讨论】:

  • 您确定test.xml真的看起来像问题中的示例 xml 吗?该示例没有 inkspace 的命名空间声明,ET 将返回“未绑定前缀”错误。
  • 谢谢@JackFleeting - 事实上,我的实际文件是一个inkspace,但由于我忘记了我过去可能知道的关于 XML(和名称空间)的一切,我很惊讶地发现做一个“最小的例子”不会由于 XML 命名空间前缀,一般工作。所以 Python 代码显示了在这两种情况下如何处理 - 我想知道是否可以使用特殊开关 xmlstarlet 来做同样的事情(即处理除了属性前缀之外没有命名空间信息的最小 XML 文件,和一个“真实的”“正确命名空间”的 XML 文件)

标签: xml xmlstarlet


【解决方案1】:

可以使用xmllint 分三步完成:

  1. 获取标签值
  2. 构建 XPath 以设置 @id 值
  3. 执行XPath表达式
        # Step 1 - get label values into an array for elements containing both attributes
        labels=( $(printf '%s
    ' 'setrootns' 'cat //*[@inkscape:label and @id]/@inkscape:label' | xmllint --shell tmp.xml | sed -rne '/inkscape:label/ s/inkscape:label="(.*)"//p' ) )
    
        # Step 2 - build xpath
        xpath=( 'setrootns' )
        for i in "${!labels[@]}"; do
            # get current element name ;-)
            xpath[${#xpath[@]}]="xpath name((//*[@inkscape:label and @id])[$i+1])"
            xpath[${#xpath[@]}]="cd (//*[@inkscape:label and @id])[$i+1]/@id"
            xpath[${#xpath[@]}]="set ${labels[$i]}"
        done
        xpath[${#xpath[@]}]='save'
        xpath[${#xpath[@]}]='bye'
    
        #Step 3 - execute Xpath
        printf "%s
    " "${xpath[@]}" | xmllint --shell tmp.xml
    

    最重要的 XPath 表达式是查找具有两个属性的元素的表达式,其中节点集索引是 labels 数组索引 + 1

    (//*[@inkscape:label and @id])[$i+1]
    

    脚本输出

    / > setrootns
    / > xpath name((//*[@inkscape:label and @id])[0+1])
    Object is a string : g
    / > cd (//*[@inkscape:label and @id])[0+1]/@id
    id > set hello
    id > xpath name((//*[@inkscape:label and @id])[1+1])
    Object is a string : circle
    id > cd (//*[@inkscape:label and @id])[1+1]/@id
    id > set there
    id > xpath name((//*[@inkscape:label and @id])[2+1])
    Object is a string : rect
    id > cd (//*[@inkscape:label and @id])[2+1]/@id
    id > set world
    id > xpath name((//*[@inkscape:label and @id])[3+1])
    Object is a string : circle
    id > cd (//*[@inkscape:label and @id])[3+1]/@id
    id > set more
    id > save
    id > bye
    

【讨论】:

    【解决方案2】:
    本着如何假设问题的精神 - 我如何使用 xmlstarlet 实现这一目标?

    输入文件未定义导致的inkscape 命名空间 XML 解析器 () 发出消息并解析 inkscape:label 节点作为属于空命名空间的属性。 回想一下,组件名称中的 :(冒号)是 tolerated 但是unrecommended,以及默认命名空间 doesn't 适用于属性名称。

    要产生所需的输出,您可以说,

    xmlstarlet -q edit 
      -u '//*[@*[local-name()="inkscape:label"][namespace-uri()=""]]/@id' 
      -x 'string(../@*[local-name()="inkscape:label"][namespace-uri()=""])' 
    file.xml
    

    在哪里

    • 全局-q选项抑制 来自解析器的关于缺少名称空间定义的消息
    • local-name()namespace-uri() 在没有前缀的特殊情况下用作解决方法 名称包含 :(冒号),因为 @inkscape:label 会导致 解析器寻找不存在的inkscape命名空间

    由于 inkscape 在这里的大多数上下文中可能是名称空间前缀 是 2 种替代方法。你可以让xmlstarlet添加一个缺失的 通过使用 edit 修改输入来命名空间节点,

    xmlstarlet -q edit 
      -s '*' -t attr -n 'xmlns:inkscape' -v 'http://www.inkscape.org/namespaces/inkscape' 
    file.xml |
    xmlstarlet edit -u '//*[@inkscape:label]/@id' -x 'string(../@inkscape:label)'
    

    pyx … | sed … | depyx -管道,

    xmlstarlet -q pyx file.xml |
    sed '1a
    Axmlns:inkscape http://www.inkscape.org/namespaces/inkscape' |
    xmlstarlet depyx - |
    xmlstarlet edit -u '//*[@inkscape:label]/@id' -x 'string(../@inkscape:label)'
    

    输出 XML 的根元素将包含 inkscape 命名空间 节点。要生成所需的输出(减去 XML 声明), 命名空间节点可以通过追加删除 | xmlstarlet pyx - | sed '/^Axmlns:inkscape /d' | xmlstarlet depyx - 到上一个命令。 (xmlstarlet edit 无法删除命名空间节点。)

    将 XML 转换为 PYX 处理过程中的符号有时对简单的查询很有用,或者 编辑非复杂数据,但无法访问 XPath。 注意xmlstarletpyxdepyx命令不保证准确的往返。 depyx,例如输出非折叠的空 元素(例如&lt;void&gt;&lt;/void&gt;)所以额外通过xmlstarlet format 有时是有序的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 2023-04-07
      • 1970-01-01
      • 1970-01-01
      • 2010-11-12
      相关资源
      最近更新 更多