【问题标题】:Perl remove duplicate XML tagsPerl 删除重复的 XML 标签
【发布时间】:2019-11-15 15:43:51
【问题描述】:

我有以下 XML 文件:

<d:entry id="a" d:title="a">
  <d:index d:value="a" d:title="a"/>
  <d:index d:value="b" d:title="b"/>
  <d:index d:value="a" d:title="a"/>
  <d:index d:value="c" d:title="c"/>
  <d:index d:value="b" d:title="b"/>
  <d:index d:value="a" d:title="a"/>
  <d:index d:value="b" d:title="b"/>
  <div>This is the content for entry.</div>
</d:entry>
<d:entry id="b" d:title="b">
  <d:index d:value="a" d:title="a"/>
  <d:index d:value="b" d:title="b"/>
  <div>This is the content for entry.</div>
</d:entry>

(为便于阅读添加了空格。)

&lt;d:index 有一些重复项,我需要删除所有重复项,只保留一个唯一的&lt;d:index。想要的效果是这样的:

<d:entry id="a" d:title="a">
   <d:index d:value="a" d:title="a"/>
   <d:index d:value="b" d:title="b"/>
   <d:index d:value="c" d:title="c"/>
   <div>This is the content for entry.</div>
</d:entry>
<d:entry id="b" d:title="b">
  <d:index d:value="a" d:title="a"/>
  <d:index d:value="b" d:title="b"/>
  <div>This is the content for entry.</div>
</d:entry>

为此,我可以在某些编辑器中进行正则表达式替换,但需要多次完成,我想知道 Perl 是否有一些方法可以一次性完成。

【问题讨论】:

  • metacpan.org/pod/Catmandu::Fix 可能可以做到,但我不知道如何随手。很抱歉!
  • 这看起来很有趣。浏览了一会儿,我没有找到正确的 util 方法来删​​除重复索引。
  • 您使用的是什么解析器?到目前为止你的代码是什么?
  • 我没有使用任何解析器。我认为只要 perl + regex 就可以了……抱歉,我还是 Perl 新手,不熟悉 Perl 中的一些库。

标签: xml perl command-line duplicates


【解决方案1】:

以下是过滤掉重复项的常用方法:

my @filtered = grep { !$seen{$_}++ } @unfiltered;

这可以根据你的需要进行调整,如下面的sn-p所示:

my %seen;
for my $index_node ($xpc->findnodes('d:index', $entry_node)) {
   my $value = $xpc->findvalue('@d:value', $index_node);
   my $title = $xpc->findvalue('@d:title', $index_node);
   if ($seen{$value}{$title}++) {
      $index_node->unbind();
   }
}

(我使用了我的首选解析器 XML::LibXML,因为您没有提及您使用的是哪个解析器。)

【讨论】:

  • 感谢@ikegami 的回答。这可以用 Perl oneliner 编写吗? :)
  • 我不知道任何现有的工具,不。此外,工具建议的请求是题外话。
  • 在我导入XML::LibXML后,尝试运行上面的sn-p,它给出了错误,Global symbol "$xpc" requires explicit package name (did you forget to declare "my $xpc"?) at 1.pl line 7. Global symbol "$entry_node" requires explicit package name (did you forget to declare "my $entry_node"?) at 1.pl line 7.我不知道我是否错过了一些要导入的模块,或者我应该声明 $xpc。
  • 您需要一个 XML::LibXML::XPathContext 对象来定义dSee here
  • 我明白了,谢谢@ikegami。知道这真的很有帮助。:) 另一个困惑是我们如何获得$entry_node。在当前for loop 之外是否还有另一个for loop 来获取每个$entry_node
【解决方案2】:

任何了解 XML 的人都会告诉您不要使用正则表达式处理,而是使用适当的 XML 解析器和 XML 工具。如果您知道文件的格式将始终与您显示的完全相同,则可以使用正则表达式(尽管不是我)来完成,例如换行符和双引号以及属性顺序与您的示例完全相同。但是如果你把它投入生产,那么生成 XML 的人会在一年后在 StackOverflow 上询问如何确保他们可以精确地生成这种格式的 XML,因为如果属性顺序错误或接收应用程序会中断,或者使用单引号而不是双引号。所以你正在为未来制造问题。 (请记住 Postel 定律,在这种情况下,这意味着您应该接受与此 XML 等效的任何格式良好的 XML)。

无论如何,在 XSLT 中执行此操作比您建议的方式要容易得多。假设您希望两个属性都匹配以使元素计为重复项,则代码为:

<xsl:template match="d:entry">
<xsl:copy>
  <xsl:for-each-group select="d:index" 
                      group-by="concat(@d:value, '~', @d:title)">
     <xsl:copy-of select="current-group()[1]"/>
  </xsl:for-each-group>
  <xsl:copy-of select="div"/>
</xsl:copy>
</xsl:template>

顺便说一句,您说“为可读性添加了空格”。那个空格,特别是如果它包含换行符,将对任何正则表达式解决方案产生重大影响,但对正确编写的 XSLT 完全没有影响。

【讨论】:

  • 谢谢@Michael Key 我正在学习这种XSLT 方法,但它给出了以下错误:compilation error: file remove_duplicate_indices.xsl element for-each-group xsltStylePreCompute: unknown xsl:for-each-group。我正在使用XML::LibXSLT 来做这件事。
  • 我稍微更改了 xsl 文件,它不再抛出错误了。但结果出乎意料;它删除了所有的&lt;d:index,只留下两个div的内容,没有&lt;div作为标签。
  • 刚刚发布了一篇关于此的新帖子。 stackoverflow.com/q/56919764/1118630
  • 我应该提到要使用 xsl:for-each-group 您需要一个 XSLT 2.0 处理器。 XSLT 2.0 于 12 年前问世,但遗憾的是,一些处理器,如 LibXSLT,从未更新。
  • 谢谢@Michael Kay。我尝试安装PerlXSLT 2.0 处理器,但没有成功。我想我稍后会尝试XSLT 方法。
【解决方案3】:

使用Mojo::DOM:

perl -MMojo::DOM -0777 -E'my $dom = Mojo::DOM->new->xml(1)->parse(<>);
  $dom->find(q{d\\:entry})->each(sub { my %seen;
    $_->find(q{d\\:index})->each(sub {
      $_->remove if $seen{$_->{"d:value"}}{$_->{"d:title"}}++ }) });
  print $dom->to_string' input.xml

结果:

<d:entry d:title="a" id="a">
  <d:index d:title="a" d:value="a" />
  <d:index d:title="b" d:value="b" />

  <d:index d:title="c" d:value="c" />



  <div>This is the content for entry.</div>
</d:entry>
<d:entry d:title="b" id="b">
  <d:index d:title="a" d:value="a" />
  <d:index d:title="b" d:value="b" />
  <div>This is the content for entry.</div>
</d:entry>
  • 如果实际内容没有这样的空格,去掉标签后就不会留下。否则,更多的逻辑可以删除空白文本节点。
  • 我会为此使用 ojo,但它没有用于 XML 模式解析的快捷方式。
  • 如果 XML 包含任何非 ascii 字符,则需要在 STDIN 上对其进行解码,并根据其编码在 STDOUT 上对其进行编码;如果是通常的 UTF-8,您可以使用 -CS 开关来执行此操作。

【讨论】:

  • 我刚刚意识到这是在全局删除重复的&lt;d:index。如果它需要在&lt;d:entry 范围内怎么办。即不同的条目实际上可以具有相同的&lt;d:index,在这种情况下,全局重复的&lt;d:index 在一个&lt;d:entry 的范围内不算重复。
  • 我试过这个,但它给出了一个错误:perl -MMojo::DOM -0777 -E'my $dom = Mojo::DOM-&gt;new-&gt;xml(1)-&gt;parse(&lt;&gt;); $dom-&gt;find(q{d\\:entry})-&gt;each(my %seen, $_-&gt;find(q{d\\:index})-&gt;each(sub { $_-&gt;remove if $seen{$_-&gt;{"d:value"}}{$_-&gt;{"d:title"}}++ })); print $dom-&gt;to_string;' input.xml - 错误消息:Can't call method "find" on an undefined value
  • @jonah_w 这不适合打电话给each。试试我的更新版本。
  • 哇,这就是 Mojo::DOM 处理命名空间的方式?哎哟!永远不必依赖于知道将要使用的前缀(或缺少前缀)。必须知道命名空间。
  • @ikegami 这只是一种选择。您还可以将命名空间别名传递给 find,但这似乎超出了单行的范围,并且 OP 中未提供此类信息。
猜你喜欢
  • 2020-09-02
  • 2016-09-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多