【问题标题】:Delete specific XML elements after finding specific test condition within the element start/end tag在元素开始/结束标记中找到特定测试条件后删除特定 XML 元素
【发布时间】:2018-02-18 23:10:27
【问题描述】:

我希望创建一个快速的 shell 脚本(HP-UX 系统)来根据一个简单的测试条件删除 XML 标记。我不能使用像“xmlstarlet”这样的 XML 感知工具,因为出于多种原因,这些工具在我的生产系统上不可用。我确实知道这些是正确的方法,但在这件事上我别无选择。

考虑以下与两个设备相关的两个 XML 元素。当设备无法通信时,将没有 StationId 和 HardwareInv,标签与最后的 <..../> 不同。当设备处于通讯状态,StationId 存在且 HardwareInv 内容可用时,开始/结束标签是完整的,即</....> 在结束。

我想通过搜索<StationId/> 和/或<HardwareInv/> 来查找和删除通信外的设备,如果找到,则完全删除关联的DeviceA 标签之间的所有内容,包括DeviceA 标签本身,不留空白排在后面。

我已经尝试了一些不同的结果,特别是使用“sed”,但没有 100% 成功。非常感谢您的帮助。

这是输入的 XML 文件:

<DeviceA>
  <PhysicalAdd>10.10.10.69</PhysicalAdd>
  <NEId>0000-Test-06</NEId>
  <StationId/>

  *** MORE CONTENT REMOVED ***

  <HardwareInv/>
</DeviceA>
<DeviceA>
  <PhysicalAdd>10.10.10.109</PhysicalAdd>
  <NEId>0000-Test-13</NEId>
  <StationId>Bravo-01</StationId>

  *** MORE CONTENT REMOVED ***

  <HardwareInv>
    <Unit>
      <UnitId>1</UnitId>
      <SerialNumber>1389A</SerialNumber>
    </Unit>
  </HardwareInv>
</DeviceA>

预期输出:

<DeviceA>
  <PhysicalAdd>10.10.10.109</PhysicalAdd>
  <NEId>0000-Test-13</NEId>
  <StationId>Bravo-01</StationId>

  *** MORE CONTENT REMOVED ***

  <HardwareInv>
    <Unit>
      <UnitId>1</UnitId>
      <SerialNumber>1389A</SerialNumber>
    </Unit>
  </HardwareInv>
</DeviceA>

【问题讨论】:

  • @EdMorton,感谢您的意见,将更新帖子。它是大文件的一部分,将用作输入。解释中解释了预期的输出,但会以任何方式更新帖子。
  • @EdMorton,很遗憾没有可用的 GNU awk。

标签: xml shell awk sed


【解决方案1】:

这个脚本很简单,可以使用任何版本的 awk:

awk '
/<DeviceA>/          { found = 0; tosave = 1; save = "" }
/<HardwareInv\/>/ || /<StationId\/>/        { found = 1 }
/<DeviceA>/,/<\/DeviceA>/   { save = save $0 "\n" }
tosave==0            { print }
/<\/DeviceA>/        { if(!found)printf "%s",save; tosave = 0 }
'

它检测起始标签并将两个布尔值设置为false,0和true,1,并清除字符串变量save
当找到空标签时,found 布尔值设置为 true。 要删除的组的开始和结束标记之间的所有行都累积在字符串变量中,它们之间有一个换行符。

如果不保存行,则打印它们。当结束标签匹配时, 如果没有找到空标签,打印保存的组,并停止保存。

代码中有一些冗余,但为了保持简单。显然,这仅处理您提供的格式的数据,不适用于任何 xml。

【讨论】:

  • 我发现了一个以前没有遇到过的 POSIX awk(HP-UX 系统)问题,似乎有 300 字节的硬限制(这是错误:“awk: Format item %s 不能超过 3,000 字节”。)针对“真实”文件运行脚本时。显然,显示的示例缺少内容,因此在给出的示例中不明显。我确实设法发现我们的两个 HP-UX 系统毕竟确实有 gawk 并且作为 gawk 脚本运行,都像梦一样工作(没有 POSIX awk 的限制),所以谢谢你的解决方案!
  • 有趣。如果问题只是因为%s 太长,简单的答案是将printf "%s",save 替换为print substr(save,1,length(save)-1)。我只是用它来删除最后一个换行符,因为 print 无论如何都会添加一个最后一个换行符。
  • 是的,最后一个建议适用于 POSIX awk :-) 很好,需要稍长的时间才能完成,但再次工作正常,非常感谢!
【解决方案2】:

对于多字符 RS 使用 GNU awk 会更简洁:

$ awk -v RS='</DeviceA>\\s*' -v ORS= '/<StationId>/{print $0 RT}' file
<DeviceA>
  <PhysicalAdd>10.10.10.109</PhysicalAdd>
  <NEId>0000-Test-13</NEId>
  <StationId>Bravo-01</StationId>

  *** MORE CONTENT REMOVED ***

  <HardwareInv>
    <Unit>
      <UnitId>1</UnitId>
      <SerialNumber>1389A</SerialNumber>
    </Unit>
  </HardwareInv>
</DeviceA>

但是对于任何 awk,您只需要先逐行建立记录:

$ cat tst.awk
{ rec = (rec=="" ? "" : rec ORS) $0 }
/<\/DeviceA>/ {
    if (rec ~ /<StationId>/) {
        print rec
    }
    rec = ""
}

$ awk -f tst.awk file
<DeviceA>
  <PhysicalAdd>10.10.10.109</PhysicalAdd>
  <NEId>0000-Test-13</NEId>
  <StationId>Bravo-01</StationId>

  *** MORE CONTENT REMOVED ***

  <HardwareInv>
    <Unit>
      <UnitId>1</UnitId>
      <SerialNumber>1389A</SerialNumber>
    </Unit>
  </HardwareInv>
</DeviceA>

【讨论】:

  • 感谢您花时间提供解决方案。根据其他解决方案中的评论,POSIX awk 存在问题。我尝试了这个解决方案,但如果没有生成任何输出,即使使用 gawk 也是如此。我还没有分析为什么,但其他解决方案确实有效,尽管使用 gawk。
  • 因为您确实有 GNU awk,所以我添加了 GNU awk 解决方案,并放宽了记录末尾的文本以允许尾随空格并将 printf 替换为 print。
  • 是的,我同意,您的解决方案适用于粘贴的内容。我接受 67 MB 且不断增长的“真实”文件包含更多内容,并且发布的任何潜在解决方案都可能因此失败。我发布了我确定可以捕获/寻找的“变化”的部分。我创建了一个更大的脚本,但在这个特定位上只需要比我自己更了解 sed/awk 知识的人的帮助。非常感谢您的帮助,非常感谢!
  • 将测试新的解决方案并做出回应;-)
  • 完美,使用 'gawk' 就像做梦一样(认为您打算将它放在上面的 GNU awk 解决方案中,而不是 'awk')。非常感谢!