使用 sed 从类似 XML 的文件中删除特定文本

Posted

技术标签:

【中文标题】使用 sed 从类似 XML 的文件中删除特定文本【英文标题】:Remove specific text from an XML like file using sed 【发布时间】:2021-09-19 07:15:43 【问题描述】:

我有以下文件(这是一个 JUnit 报告文件),我需要从中删除 system-outsystem-err 节点及其内容,同时保留其他节点结构(元素和值)。

我的文件具有以下类型的结构和内容(请注意system-* 元素可以有多行内容和类似html的标签):

<testsuite name="someTest" tests="1" skipped="0" failures="0" errors="0">
  <properties/>
  <testcase name="someMethod" classname="classA" time="0.096">
    <system-out><![CDATA[foo <li></li> bar]]></system-out>
    <system-err><![CDATA[[one] INFO two
three four 
five]]></system-err>
  </testcase>
  <system-out><![CDATA[]]></system-out>
  <system-err><![CDATA[]]></system-err>
</testsuite>

想要的结果是有

<testsuite name="someTest" tests="1" skipped="0" failures="0" errors="0">
  <properties/>
  <testcase name="someMethod" classname="classA" time="0.096">
  </testcase>
</testsuite>

我已经尝试了多种 sed 模式变体,以下不是很好,但部分有效。当前的方法是使用tr 用一些外来字符替换新行,然后在一行文本上应用sed,然后重用tr 以包含以前的新行(我结合了几个SO建议来拥有它和我真的不知道如何使用多个sed -N 标志):

tr "\n" "\f" < "$f" |
sed 's/\(<system-err>\)\(.*\)\(<\/system-err>\)/\1\3/' |
sed 's/\(<system-out>\)\(.*\)\(<\/system-out>\)/\1\3/' |
tr "\f" "\n" > $(basename "$f")-out.xml

这样做的问题是 sed 是贪婪的,例如将从第一个系统错误删除到最后一个,留下未关闭的元素。 我尝试了多种方法,还使用sed -E 's/&lt;system-out&gt;&lt;![(.*)]&gt;&lt;\/system-out&gt;//g' 模式来匹配system-* 文本之间的任何内容,但它并没有真正起作用。

我不是 sed 或 regexp 专家,所以请多多包涵 :)。我的限制是需要使用 sed(在 bash 脚本中)。

有人可以告诉我如何实现删除。

提前谢谢你!

【问题讨论】:

Don't Parse XML/HTML With Regex. 我建议使用 XML/HTML 解析器 (xmlstarlet, xmllint ...)。 【参考方案1】:

与xidel:

$ xidel -s input.xml -e '
  x:replace-nodes(/,(//system-out,//system-err),())
' --output-node-format=xml --output-node-indent
<testsuite name="someTest" tests="1" skipped="0" failures="0" errors="0">
  <properties/>
  <testcase name="someMethod" classname="classA" time="0.096">
  </testcase>
</testsuite>

【讨论】:

【参考方案2】:

sed

警告:如果文件的结构稍有不同,则很有可能无法正常工作。

sed -e '\|<system-out>.*</system-out>|d' \
    -e '\|<system-err>.*</system-err>|d' \
    -e '\|<system-err>|,\|</system-err>|d' file.xml

我从// 切换到\||

输出:

<testsuite name="someTest" tests="1" skipped="0" failures="0" errors="0">
  <properties/>
  <testcase name="someMethod" classname="classA" time="0.096">
  </testcase>
</testsuite>

【讨论】:

【参考方案3】:

仅作记录,xmlstarlet 不适用于大文件(即对于 30+ MB 大小的文件,它会引发“巨大的输入查找”错误)。但是对于我最初的问题中的小用例来说,这非常棒,所以 Cyrus 的回答成功了。

如果有人需要处理较大文件的东西,如上所述(我个人也需要可扩展的东西),我找到了一个与 Python 相关的直接解决方案(所以这里也没有sed):

import xml.etree.ElementTree as ET

file = "myJunitReport.xml"    
tree = ET.parse(file)
root = tree.getroot()

# remove top level system-out/system-err
for elem in root.findall('system-out'):
    root.remove(elem)
for elem in root.findall('system-err'):
    root.remove(elem)

# remove testcase related system-out/system-err
for child in root.findall("testcase"):
    for profile in child.findall(".//system-out"):
        child.remove(profile)
    for profile in child.findall(".//system-err"):
        child.remove(profile)

tree.write(file)

一个重要的部分是我使用的是 Python 的默认 XML ElementTree API。其他解决方案,例如 lxml.etree 也会抱怨大文件。

真心希望这可以帮助其他在这种情况下苦苦挣扎的人。

【讨论】:

【参考方案4】:

使用 xmlstarlet:

xmlstarlet edit --omit-decl --delete '//system-out' --delete '//system-err' file.xml

输出:

<testsuite name="someTest" tests="1" skipped="0" failures="0" errors="0">
  <properties/>
  <testcase name="someMethod" classname="classA" time="0.096"/>
</testsuite>

见:xmlstarlet edit --help

【讨论】:

这个答案很棒,很好而且简短(成功了)。但是,虽然我知道将 sed 用于 XML/XHTML 之类的解析并不是最好的主意(至少可以这么说),但有没有办法使用 sed 呢? (我知道这可能被认为是“矫枉过正”) @avostache sed 使用 xml 并不过分;这更像是用螺丝刀敲钉子。 虽然我想要一个与 sed 相关的答案,但这个 xmlstarlet 非常完美,所以我会将其标记为已接受(此外,我知道这是处理 XML 文件处理的推荐做法)。谢谢你的帮助!

以上是关于使用 sed 从类似 XML 的文件中删除特定文本的主要内容,如果未能解决你的问题,请参考以下文章

使用 Bash 从 XML 文件中删除文本

从特定字符开始删除所有文本

从文本文件中删除 Unicode 字符 - sed ,其他 Bash/shell 方法

使用 Bash (sed?) 删除包含特定文本 (regex) 的多行 /* ... */ 样式注释

sed编辑命令

通过 Bash 从文本文件中删除空行,包括空格字符 [重复]