【问题标题】:Delete a SPECIFIC duplicate line from XML file in place从 XML 文件中删除特定的重复行
【发布时间】:2016-08-13 19:43:37
【问题描述】:

我一直在阅读有关删除整个堆栈中的重复行的信息。有 perl、awk 和 sed 解决方案,但是没有我想要的那么具体,我不知所措。

我想使用快速的 bash/shell perl 命令从这个 XML 案例中删除重复的 <path> 标记。保留所有其他重复行(如 <start><end>)完好无损!

输入 XML:

  <package>
    <id>1523456789</id>
    <models>
      <model type="A">
        <start>2016-04-20</start>      <------ Duplicate line to keep 
        <end>2017-04-20</end>          <------ Duplicate line to keep
      </model>
      <model type="B">                 
        <start>2016-04-20</start>      <------ Duplicate line to keep
        <end>2017-04-20</end>          <------ Duplicate line to keep
      </model>
    </models>
    <userinterface>
      <upath>/Example/Dir/Here</upath>
      <upath>/Example/Dir/Here2</upath>
      <upath>/example/dir/here</upath>   <------ Duplicate line to REMOVE
    </userinterface>
  </package>

到目前为止,我已经能够抓取重复的行,但不知道如何删除它们。以下

grep -H path *.[Xx][Mm][Ll] | sort | uniq -id

给出结果:

test.xml:          <upath>/example/dir/here</upath>

我现在如何删除该行?

执行下面的 perl 版本或 awk 版本也会删除 &lt;start&gt;&lt;end&gt; 日期。

perl -i.bak -ne 'print unless $seen{lc($_)}++' test.xml
awk '!a[tolower($0)]++' test.xml > test.xml.new

【问题讨论】:

  • 旁白:[Xx][Mm][Ll] 很傻。为什么不一直使用小写.xml
  • &lt;start&gt;... 以何种方式与&lt;end&gt;... 重复?您最终的 awk 解决方案应该可以正常工作。
  • @John,有些 XML 文件是大写的。
  • @WilliamPursell 注意有两个开始和两个结束标签,但用于不同的模型类型:A 和 B。
  • @WilliamPursell &lt;start&gt;2016-04-20&lt;/start&gt;&lt;end&gt;2017-04-20&lt;/end&gt; 在文件中各出现两次。

标签: bash perl shell awk sed


【解决方案1】:

如果您只想忽略紧挨着的重复行,您可以存储前一行并与之进行比较。对于忽略大小写,您可以在两侧比较中使用tolower()

awk '{ if (tolower(prev) != $0) print; prev = $0 }'

【讨论】:

  • 不幸的是,有时这些行不是一个接一个,中间可能有三分之一是不同的。考虑到这一点,我会更新问题。
【解决方案2】:

?看起来您正在使用 XML。要解析吗?

嘿,我以前从来没有用 Perl 做过,但是有一个 Introductory Tutorial 和一切......这不是很简单。阅读XML::SAX::ParserFactoryXML::SAX::Base 我想出了你在这个答案底部看到的代码。

问题已更新为没有相邻行;以前:

好的,我看到您有两个日期匹配的 &lt;start&gt; 标记和两个日期匹配的 &lt;end&gt; 标记在整个文件中,但它们是不同的部分。如果您的所有重复行实际上也相邻,因为它们在您的示例中 ,那么您只需要使用来自 GNU 的 uniq 命令Coreutils 或等效的。此命令可以通过正确使用 LC_COLLATE 环境变量设置来忽略大小写,但老实说,我发现很难找到示例或 read how to use LC_COLLATE 忽略大小写。

继续解析器:

#!/usr/bin/perl
use XML::SAX;

my $parser = XML::SAX::ParserFactory->parser(
    Handler => TestXMLDeduplication->new()
);

my $ret_ref = $parser->parse_file(\*TestXMLDeduplication::DATA);
close(TestXMLDeduplication::DATA);

print "\n\nDuplicates skipped: ", $ret_ref->{skipped}, "\n";
print "Duplicates cut: ", $ret_ref->{cut}, "\n";

package TestXMLDeduplication;
use base qw(XML::SAX::Base);

my $inUserinterface;
my $inUpath;
my $upathSeen;
my $defaultOut;
my $currentOut;
my $buffer;
my %seen;
my %ret;

sub new {
    # Idealy STDOUT would be an argument
    my $type = shift;
    #open $defaultOut, '>&', STDOUT or die "Opening STDOUT failed: $!";
    $defaultOut = *STDOUT;
    $currentOut = $defaultOut;
    return bless {}, $type;
}

sub start_document {
    %ret = ();
    $inUserinterface = 0;
    $inUpath = 0;
    $upathSeen = 0;
}

sub end_document {
    return \%ret;
}

sub start_element {
    my ($self, $element) = @_;

    if ('userinterface' eq $element->{Name}) {
      $inUserinterface++;
      %seen = ();
    }
    if ('upath' eq $element->{Name}) {
      $buffer = q{};
      undef $currentOut;
      open($currentOut, '>>', \$buffer) or die "Opening buffer failed: $!";
      $inUpath++;
    }

    print $currentOut '<', $element->{Name};
    print $currentOut attributes($element->{Attributes});
    print $currentOut '>';
}

sub end_element {
    my ($self, $element) = @_;

    print $currentOut '</', $element->{Name};
    print $currentOut '>';

    if ('userinterface' eq $element->{Name}) {
      $inUserinterface--;
    }

    if ('upath' eq $element->{Name}) {
      close($currentOut);
      $currentOut = $defaultOut;
      # Check if what's in upath was seen (lower-cased)
      if ($inUserinterface && $inUpath) {
    if (!exists $seen{lc($buffer)}) {
          print $currentOut $buffer;
    } else {
      $ret{skipped}++;
      $ret{cut} .= $buffer;
    }
    $seen{lc($buffer)} = 1;
      }
      $inUpath--;
    }
}

sub characters {
    # Note that this also capture indentation and newlines between tags etc.
    my ($self, $characters) = @_;

    print $currentOut $characters->{Data};
}

sub attributes {
    my ($attributesRef) = @_;
    my %attributes = %$attributesRef;

    foreach my $a (values %attributes) {
        my $v = $a->{Value};
      # See also XML::Quote
      $v =~ s/&/&amp;/g;
      $v =~ s/</&lt;/g;
      $v =~ s/>/&gt;/g;
      $v =~ s/"/&quot;/g;
    print $currentOut ' ', $a->{Name}, '="', $v, '"';
    }
}

__DATA__
  <package>
    <id>1523456789</id>
    <models>
      <model type="A">
        <start>2016-04-20</start>   
        <end>2017-04-20</end>    
      </model>
      <model type="B">                 
        <start>2016-04-20</start>     
        <end>2017-04-20</end>        
      </model>
    </models>
    <userinterface>
      <upath>/Example/Dir/Here</upath>
      <upath>/Example/Dir/Here2</upath>
      <upath>/example/dir/here</upath>   
    </userinterface>
    <userinterface>
      <upath>/Example/Dir/<b>Here</b></upath> <upath>/Example/Dir/Here2</upath>
      <upath>/example/dir/<b>here</b></upath>   
    </userinterface>
  </package>

这不再按行工作,而是在userinterface 标记内找到upath 标记,如果它们在该父组中重复,则会将其删除。保留周围的缩进和换行符。如果upath 标签中有upath 标签,那也会有点奇怪。

看起来像这样:

$ perl saxEG.pl
<package>
    <id>1523456789</id>
    <models>
      <model type="A">
        <start>2016-04-20</start>
        <end>2017-04-20</end>
      </model>
      <model type="B">
        <start>2016-04-20</start>
        <end>2017-04-20</end>
      </model>
    </models>
    <userinterface>
      <upath>/Example/Dir/Here</upath>
      <upath>/Example/Dir/Here2</upath>

    </userinterface>
    <userinterface>
      <upath>/Example/Dir/<b>Here</b></upath> <upath>/Example/Dir/Here2</upath>

    </userinterface>
  </package>
Duplicates skipped: 2
Duplicates cut: <upath>/example/dir/here</upath><upath>/example/dir/<b>here</b></upath>

【讨论】:

    【解决方案3】:

    以下脚本接受 XML 文件作为第一个参数,使用 xmlstarlet(脚本中的 xml)解析 XML 树并使用 Associative Array(需要 Bash 4)来解析存储唯一的&lt;upath&gt; 节点值。

    #!/bin/bash
    
    input_file=$1
    # XPath to retrieve <upath> node value.
    xpath_upath_value='//package/userinterface/upath/text()'
    # XPath to print XML tree excluding  <userinterface> part.
    xpath_exclude_userinterface_tree='//package/*[not(self::userinterface)]'
    # Associative array to help us remove duplicated <upath> node values.
    declare -A arr
    
    print_userinterface_no_dup() { 
        printf '%s\n' "<userinterface>"
        printf '<upath>%s</upath>\n' "${arr[@]}"
        printf '%s\n' "</userinterface>"
    }
    
    # Iterate over each <upath> node value, lower-case it and use it as a key in the associative array.
    while read -r upath; do
        key="${upath,,}"
        # We can remove this 'if' statement and simply arr[$key]="$upath"
        # if it doesn't matter whether we remove <upath>foo</upath> or <upath>FOO</upath>
        if [[ ! "${arr[$key]}" ]]; then
            arr[$key]="$upath"
        fi
    done < <(xml sel -t -m "$xpath_upath_value" -c \. -n "$input_file")
    
    printf '%s\n' "<package>"
    
    # Print XML tree excluding <userinterface> part.
    xml sel -t -m "$xpath_exclude_userinterface_tree" -c \. "$input_file"
    
    # Print <userinterface> tree without duplicates.
    print_userinterface_no_dup
    
    printf '%s\n' "</package>"
    

    测试(脚本名称为sof):

    $ ./sof xml_file
    <package>
        <id>1523456789</id>
        <models>
          <model type="A">
            <start>2016-04-20</start>
            <end>2017-04-20</end>
          </model>
          <model type="B">                 
            <start>2016-04-20</start>
            <end>2017-04-20</end>
          </model>
        </models>
        <userinterface>
            <upath>/Example/Dir/Here2</upath>
            <upath>/Example/Dir/Here</upath>
        </userinterface>
    </package>
    

    如果我的 cmets 没有为您提供足够清晰的代码,请询问,我会相应地回答并编辑此解决方案。


    我的xmlstarlet 版本是1.6.1,针对libxml2 2.9.2libxslt 1.1.28 编译。

    【讨论】:

    • 值得 +1 只是因为它建议使用 XML 解析器,而不是正则表达式。
    【解决方案4】:
    $ awk '!(/<upath>/ && seen[tolower($1)]++)' file
      <package>
        <id>1523456789</id>
        <models>
          <model type="A">
            <start>2016-04-20</start>      <------ Duplicate line to keep
            <end>2017-04-20</end>          <------ Duplicate line to keep
          </model>
          <model type="B">
            <start>2016-04-20</start>      <------ Duplicate line to keep
            <end>2017-04-20</end>          <------ Duplicate line to keep
          </model>
        </models>
        <userinterface>
          <upath>/Example/Dir/Here</upath>
          <upath>/Example/Dir/Here2</upath>
        </userinterface>
      </package>
    

    【讨论】:

    • 哈哈哈,Drive-By Downvoter...轻笑。我不知道是谁,但我找到了你,伙计!这是我正在寻找的答案。是的,我可以为它编写一个程序,但在我的情况下,它是一个补丁,直到我的软件的下一个版本。我必须每天手工编辑这些 XML,因为它们只是为了让它们通过我的应用程序。我需要一个单行来放入一个 cron,直到我发现错误并修复它。是的,当然我可以使用 XMLStarlet 和 Perl XML 来 /program/ 它,但是为什么要编写一个程序,而只有一个班轮可以呢? !谢谢!
    • 如果您可以内联执行此操作,可以获得奖励积分吗?我需要将它应用到 XML 目录而不使用 for 循环和临时文件,如下所示:for afile in *.xml; do awk '...' $afile &gt; to $afile.tmp &amp;&amp; mv $afile.tmp $afile。似乎大师会想出一个更好的方法,我没有看到 awk 文件到位。
    • 使用 GNU awk 只需添加 -i inplace 标志。否则,tmp 文件就是这样做的方法。还可以考虑使用find ... -print0 | xargs -0 而不是 for 循环。
    【解决方案5】:

    如果您正在解析 XML,那么您真的应该使用解析器。有多种选择 - 但不要使用正则表达式,因为它们是通往非常脆弱的代码的途径 - 出于您发现的所有原因。

    请参阅:parsing XML with regex

    但总而言之,XML 是一种上下文语言。正则表达式不是。 XML 中也有一些完全有效的差异,它们在语义上是相同的,正则表达式无法处理。

    例如一元标签、可变缩进、不同位置的标签路径和换行。

    我可以用一堆不同的方式格式化你的源 XML - 所有这些都是有效的 XML,说同样的事情。但这会破坏基于正则表达式的解析。这是要避免的事情——有一天,神秘地,您的脚本会因为在 XML 规范中有效的上游更改而无缘无故地中断。

    这就是你应该使用解析器的原因:

    我喜欢XML::Twig,它是一个perl 模块。你可以做你想做的事情:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    
    use XML::Twig; 
    
    my %seen; 
    
    #a subroutine to process any "upath" tags. 
    sub process_upath {
       my ( $twig, $upath ) = @_; 
       my $text = lc $upath -> trimmed_text;
       $upath -> delete if $seen{$text}++; 
    }
    
    #instantiate the parser, and configure what to 'handle'. 
    my $twig = XML::Twig -> new ( twig_handlers => { 'upath' => \&process_upath } );
       #parse from our data block - but you'd probably use a file handle here. 
       $twig -> parse ( \*DATA );
       #set output formatting
       $twig -> set_pretty_print ( 'indented_a' );
       #print to STDOUT.
       $twig -> print;
    
    __DATA__
      <package>
        <id>1523456789</id>
        <models>
          <model type="A">
            <start>2016-04-20</start>   
            <end>2017-04-20</end>    
          </model>
          <model type="B">                 
            <start>2016-04-20</start>     
            <end>2017-04-20</end>        
          </model>
        </models>
        <userinterface>
          <upath>/Example/Dir/Here</upath>
          <upath>/Example/Dir/Here2</upath>
          <upath>/example/dir/here</upath>   
        </userinterface>
      </package>
    

    这是用于说明概念的长格式,它输出:

    <package>
      <id>1523456789</id>
      <models>
        <model type="A">
          <start>2016-04-20</start>
          <end>2017-04-20</end>
        </model>
        <model type="B">
          <start>2016-04-20</start>
          <end>2017-04-20</end>
        </model>
      </models>
      <userinterface>
        <upath>/Example/Dir/Here</upath>
        <upath>/Example/Dir/Here2</upath>
      </userinterface>
    </package>
    

    通过parsefile_inplace 方法可以大大减少它。

    【讨论】:

    • 谢谢,但有点矫枉过正。 Ed Morton 在我的 awk 中解决了这个问题。
    • Awk 是个坏主意,因为它没有上下文。 XML 是一种上下文语言,因此这种解决方案总是很脆弱,并且容易因输入的完全有效更改而中断。见stackoverflow.com/a/1732454/2566198
    • 没关系,我只是这样做一两次,直到我修复了我的应用程序中的一个错误。否则我将手动编辑 XML。
    猜你喜欢
    • 1970-01-01
    • 2020-10-23
    • 2018-06-16
    • 1970-01-01
    • 1970-01-01
    • 2018-07-27
    • 1970-01-01
    • 2014-06-03
    相关资源
    最近更新 更多