Bash - 如果子节点的属性值不等于特定值,则删除 XML 个节点?

Bash - Remove XML nodes if the attribute value of a child node does not equal a specific value?

我有 RSS 提要,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>my feed</title>
  <link rel="self" href="http://myhomesite.com/articles/feed/"/>
  <updated>2019-11-04T12:45:00Z</updated>
  <id>http://myhomesite.com/articles/feed/?dt=2019-11-04T12:45:00Z</id>
  <entry>
    <id>id0</id>
    <link rel="alternate" type="text/html" href="https://yandex.ru/link123"/>
    <author>
      <name/>
    </author>
    <published>2019-11-04T12:45:00Z</published>
    <updated>2019-11-04T12:45:00Z</updated>
    <title type="html"><![CDATA[foo bar foo bar]]></title>
    <content type="html"><![CDATA[]]></content>
  </entry>
  <entry>
    <id>id2</id>
    <link rel="alternate" type="text/html" href="https://myhomesite.com"/>
    <author>
      <name/>
    </author>
    <published>2019-11-04T09:45:00Z</published>
    <updated>2019-11-04T09:45:00Z</updated>
    <title type="html"><![CDATA[foo bar foo bar]]></title>
    <content type="html"><![CDATA[]]></content>
  </entry>
....

我想删除所有节点 (/feed/entry) 其中 link href != http://myhomesite.com.

如何使用 Bash 删除值从指定符号开始的 XML 节点?

Bash 特征本身不太适合解析 XML.

这位著名的 Bash FAQ 声明如下:

Do not attempt [to extract data from an XML file] with , , , and so on (it leads to undesired results).

如果您还没有安装 XML Starlet,请考虑使用 XML 特定的命令行工具,例如 XMLStarlet. See download info here


解决方案:

使用 XML Starlet,您可以 运行 以下命令将所需结果输出到您的终端:

xml ed -N x="http://www.w3.org/2005/Atom" -d '//x:entry[not(child::x:link[@href="https://myhomesite.com"])]' /path/to/file.rss

注意: 上面显示的命令末尾的/path/to/file.rss部分应替换为实际.rss 文件.

解释:

上述指令部分分解如下:

  • xml - 调用 XML Starlet 命令。

  • ed - Edit/Update XML 文档。

  • -N x="http://www.w3.org/2005/Atom" - -N 选项将命名空间(即 http://www.w3.org/2005/Atom)绑定到我们“任意命名为 x.

  • -d - 删除匹配的节点。

  • '//x:entry[not(child::x:link[@href="https://myhomesite.com"])]' 表达式用于 find/match 您问题中指定的适当节点。

    all nodes (/feed/entry) where link href != http://myhomesite.com.

    如您所见,在 XPath 表达式中,我们在元素节点名称前加上 x 前缀,即 x:entryx:link 以确保我们以正确的方式寻址元素命名空间。

  • /path/to/file.rss - 源 .rss 文件的路径名。

保存结果 XML (RSS)

要保存结果 XML 您可以:

  1. --inplace 选项添加到上述命令 - 这将用所需结果覆盖原始 .rss。例如:

     xml ed --inplace -N x="http://www.w3.org/2005/Atom" -d '//x:entry[not(child::x:link[@href="https://myhomesite.com"])]' /path/to/file.rss
    
  2. 或者,使用 redirection operator (>) 并指定保存输出的位置的路径名。例如,以下复合命令会将结果保存到新文件中:

     xml ed -N x="http://www.w3.org/2005/Atom" -d '//x:entry[not(child::x:link[@href="https://myhomesite.com"])]' /path/to/file.rss > /path/to/results.rss
    

    注意:上述复合命令末尾的/path/to/results.rss应该替换为你想要保存的真实路径名新文件。

XPath 与 local-name():

鉴于您的示例源 XML (RSS) 不包含任何 QNames it's also possible to utilize XPath's local-name() 函数。这将不需要使用 XMLStarlet 的 -N 选项绑定命名空间。例如:

xml ed -d '//*[local-name() = "entry" and not(child::*[local-name() = "link"][@href="https://myhomesite.com"])]' /path/to/file.rss

重要提示:可能需要替换本[=中显示的所有示例命令中的前导xml部分201=] 改为 xmlstarlet。例如:

xmlstarlet ed -N x="http://www.w3.org/2005/Atom" -d '//x:entry[not(child::x:link[@href="https://myhomesite.com"])]' /path/to/file.rss.
^^^^^^^^^^

编辑:

鉴于您的示例 XML,也可以对默认名称空间使用简化语法,即使用 _: 而不是 x:。通过使用下划线 (_),您无需使用 -N 选项将名称空间绑定到前缀。有关此功能的更多信息,请参阅 XMLStarlet 文档中标题为 1.3. A More Convenient Solution 的部分。

例如:

xml ed -d '//_:entry[not(child::_:link[@href="https://myhomesite.com"])]' /path/to/file.rss

要进一步了解在您的源 XML 使用命名空间时使用 XMLStarlet,我建议您还阅读文档中的 Namespaces and default namespace


编辑 2:

OP 作者随后在评论中写道:

One question more. Condition [not(child::_:link[@href="myhomesite.com"])] is strict. I wanna be something like start with myhomesite.com but URI not important i.e. myhomesite.com**anything**. It's possible? [sic]

something like this.. xmlstarlet ed -N x="http://www.w3.org/2005/Atom" -d '//x:entry[not(child::x:link[matches(@href, '^https://myhomesite.com/' )]/@href)]' feed.rs

考虑将 Xpath 的 starts-with() 函数与前面给出的任何一个示例一起使用。例如:

  • 使用 -N 选项和 starts-with():

    xml ed -N x="http://www.w3.org/2005/Atom" -d '//x:entry[not(child::x:link[starts-with(@href, "https://myhomesite.com")])]' file.rss
    
  • 使用local-name()starts-with():

    xml ed -d '//*[local-name() = "entry" and not(child::*[local-name() = "link"][starts-with(@href, "https://myhomesite.com")])]' file.rss
    
  • 使用默认命名空间的简化语法,即下划线和 starts-with():

    xml ed -d '//_:entry[not(child::_:link[starts-with(@href, "https://myhomesite.com")])]' file.rss