如何根据 XML 文件中的特定属性名称删除重复的标签?

如何根据 XML 文件中的特定属性名称删除重复的标签?

如何根据“groupName”删除重复行并保留该行directoryId="1"

<Group id="123" groupName="ABC" lowerGroupName="abc" active="1" local="1" createdDate="2017-08-21 09:28:30.581" updatedDate="2017-08-21 09:28:30.581" type="GROUP" directoryId="10100"/>
<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>

答案1

我认为uniq这不一定是正确的工具,因为它适用于空格分隔或固定宽度的文件(从其仅有的两个“列”相关选项是--skip-fields和可见--skip-chars),而您这里拥有的是类似 XML 的列宽既不固定也不存在任何简单的单字符分隔符的数据(groupName原则上等的值可以包含空格)。

相反,我会使用用于处理 XML 的工具。

避免自己编写脚本的一种选择是基于 XPath 的过滤。可以从以下答案中了解如何使用 XPath 来过滤唯一性这些- 重要的语法元素是following-sibling::preceding-sibling::轴。用于评估 XPath 表达式的命令行工具可以在以下问题的答案中找到:这个问题。在我尝试过的那些中,最容易安装的是basex(建议这里)所以我将在下面使用它。

如果我正确理解你的问题,你想将具有相同行(XML元素)的行减少groupName到最后一行(或者选择带有行的行还有其他原因吗directoryId="1"?)。对于这样的 XML 文档:

<Groups>
<Group id="123" groupName="ABC" lowerGroupName="abc" active="1" local="1" createdDate="2017-08-21 09:28:30.581" updatedDate="2017-08-21 09:28:30.581" type="GROUP" directoryId="10100"/>
<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>
<Groups>

我们必须将所有内容包装在根元素 ( Groups) 中才能实现格式良好的 XML,这个要求可以通过下面的XPath表达式来实现:

/Groups/Group[not(@groupName = following-sibling::Group/@groupName)]

/Groups/Group选择要返回的元素,然后使用 中的表达式对其进行过滤[]@选择属性并following-sibling::匹配当前属性的所有后续同级元素(参见这里)。

运行它basex会产生预期的结果:

$ basex -i - '/Groups/Group[not(@groupName = following-sibling::Group/@groupName)]'

# [paste this into the terminal:]

<Groups>
<Group id="123" groupName="ABC" lowerGroupName="abc" active="1" local="1" createdDate="2017-08-21 09:28:30.581" updatedDate="2017-08-21 09:28:30.581" type="GROUP" directoryId="10100"/>
<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>
</Groups>

# [output:]

<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>

相比之下,缺点uniqbasex首先将整个 XML 文档读入内存,因此对于超过主内存大小的非常大的文件,这是不可行的。有一些 XML 处理器以流式方式操作 XML,例如 XSLT 3.0 具有流式转换,因此如果您必须处理大文件,可能有一种方法可以使用任何支持 XSLT 3.0 的处理器来完成此操作。但到那时,手动编写自己的小型流解析器可能会更容易。

答案2

假设 XML 文档格式良好,例如

<Groups>
<Group id="123" groupName="ABC" lowerGroupName="abc" active="1" local="1" createdDate="2017-08-21 09:28:30.581" updatedDate="2017-08-21 09:28:30.581" type="GROUP" directoryId="10100"/>
<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>
</Groups>

(我刚刚添加了一个名为 的根节点Groups),那么您可以使用xqXML 解析器包装器jq,来自https://kislyuk.github.io/yq/,像这样:

xq -x '.[].Group |= unique_by(."@groupName")' file.xml

Group这仅根据属性保留唯一节点groupName。将保留第一个看到的属性值节点。

将上述命令应用于顶部 XML 时的结果:

<Groups>
  <Group id="123" groupName="ABC" lowerGroupName="abc" active="1" local="1" createdDate="2017-08-21 09:28:30.581" updatedDate="2017-08-21 09:28:30.581" type="GROUP" directoryId="10100"></Group>
</Groups>

为了确保获得directoryID属性值最低的节点,请先按该值对节点进行排序,然后再对列表进行唯一化:

xq -x '.[].Group |= (sort_by(."@directoryId") | unique_by(."@groupName"))' file.xml

这会导致

<Groups>
  <Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"></Group>
</Groups>

作为参考,由于 是xq建立在 之上的jq,因此该表达式实际上应用于从 XML 文档翻译而来的 JSON 文档。然后,修改后的 JSON 文档被转换回 XML。考虑到本答案顶部的 XML,修改后的 JSON 文档如下所示:

{
  "Groups": {
    "Group": [
      {
        "@id": "123",
        "@groupName": "ABC",
        "@lowerGroupName": "abc",
        "@active": "1",
        "@local": "1",
        "@createdDate": "2017-08-21 09:28:30.581",
        "@updatedDate": "2017-08-21 09:28:30.581",
        "@type": "GROUP",
        "@directoryId": "10100"
      },
      {
        "@id": "456",
        "@groupName": "ABC",
        "@lowerGroupName": "abc",
        "@active": "1",
        "@local": "0",
        "@createdDate": "2017-08-21 09:28:30.634",
        "@updatedDate": "2017-08-21 09:28:30.634",
        "@type": "GROUP",
        "@directoryId": "1"
      }
    ]
  }
}

答案3

使用标识符识别行:grep 'groupName="ABC"'

您要从中取消选择具有排除条件的特定行:grep -v 'directoryId="1"'

这将为您提供要删除的线条。现在我们可以强制重复行并专门消除它们:

grep 'groupName="ABC"' input-file | grep -v 'directoryId="1"' > to-remove
cat input-file to-remove | sort | uniq -u > output-file

如果您想最后清理所有内容,您可以添加:

rm to-remove input-file
mv output-file input-file

警告这将重新排列您输入文件的内容。如果您只是有一个条目列表而没有其他结构,则此解决方案应该足够了。

答案4

另一个答案忽略了数据的 XML 性质,但仅在以下假设下有效:a) 这将用作“一次性”而不是在生产工作流程中,b) 每行的属性顺序完全相同c) 之前的属性中永远不会有任何带有空格的行groupName(也不会在groupName自己的值内):

这个答案展示了如何awk根据空格分隔的字段过滤掉重复项。在您的情况下,这也将是awk '!seen[$3]++',因为该groupName部分是第三个空格分隔的列。但是,如果我理解正确的话,你想要最后的每个“重复组”的行,而不是第一行(这是awk上面的一行会给您的)。要实现这一点,您可以简单地在将行tac输入到 之前颠倒行的顺序awk,然后再次颠倒它们以恢复原始顺序:

$ tac | awk '!seen[$3]++' | tac

# [paste this into the terminal:]

<Group id="123" groupName="ABC" lowerGroupName="abc" active="1" local="1" createdDate="2017-08-21 09:28:30.581" updatedDate="2017-08-21 09:28:30.581" type="GROUP" directoryId="10100"/>
<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>

# [output is:]

<Group id="456" groupName="ABC" lowerGroupName="abc" active="1" local="0" createdDate="2017-08-21 09:28:30.634" updatedDate="2017-08-21 09:28:30.634" type="GROUP" directoryId="1"/>

相关内容