【问题标题】:List only duplicate lines based on one column from a semi-colon delimited file?仅列出基于分号分隔文件中的一列的重复行?
【发布时间】:2009-09-20 02:01:45
【问题描述】:

我有一个包含一堆行的文件。 这些行中的每一行都有 8 个分号分隔的列。

我如何(在 Linux 中)返回重复的行但仅基于第 2 列? 我应该使用grep 还是别的什么?

【问题讨论】:

    标签: linux


    【解决方案1】:

    在 awk 脚本中查看我的 cmets

    $ cat data.txt 
    John Thomas;jd;301
    Julie Andrews;jand;109
    Alex Tremble;atrem;415
    John Tomas;jd;302
    Alex Trebe;atrem;416
    
    $ cat dup.awk 
    BEGIN { FS = ";" }
    
    {
        # Keep count of the fields in second column
        count[$2]++;
    
        # Save the line the first time we encounter a unique field
        if (count[$2] == 1)
            first[$2] = $0;
    
        # If we encounter the field for the second time, print the
        # previously saved line
        if (count[$2] == 2)
            print first[$2];
    
        # From the second time onward. always print because the field is
        # duplicated
        if (count[$2] > 1)
            print
    }
    

    示例输出:

    $ sort -t ';' -k 2 data.txt | awk -f dup.awk
    
    John Thomas;jd;301
    John Tomas;jd;302
    Alex Tremble;atrem;415
    Alex Trebe;atrem;416
    

    这是我的解决方案 #2:

    awk -F';' '{print $2}' data.txt |sort|uniq -d|grep -F -f - data.txt
    

    此解决方案的美妙之处在于它保留了行顺序,但代价是同时使用了许多工具(awk、sort、uniq 和 fgrep)。

    awk 命令打印出第二个字段,然后对其输出进行排序。接下来,uniq -d 命令挑选出重复的字符串。此时,标准输出包含重复的第二个字段列表,每行一个。然后我们将该列表通过管道传输到 fgrep。 '-f -' 标志告诉 fgrep 从标准输入中查找这些字符串。

    是的,你可以用命令行全力以赴。我更喜欢第二种解决方案,可以更好地使用许多工具和更清晰的逻辑(至少对我而言)。缺点是工具的数量和可能使用的内存。此外,第二种解决方案效率低下,因为它扫描数据文件两次:第一次使用 awk 命令,第二次使用 fgrep 命令。仅当输入文件很大时,这种考虑才有意义。

    【讨论】:

    • 使用单独的数组进行计数和行的第一个实例可能比我的版本使用一个数组来完成两项任务。
    • 很好的解决方案。避免预排序要求!
    • 注意不稳定的排序。它可以报告第一行与第二行重复。
    • 我在第二种解决方案中看到的障碍是,例如,如果第二个字段包含(仅)'e'并且它被重复,fgrep 命令将拾取每一行在它的任何地方都有一个'e'。您可以通过让awk 执行{print ";$2;"} 来降低出现问题的可能性;现在,如果重复的 $2 中的值也显示为 $1 或 $3..$8 中的值,您将只打印杂散线。对于'e',这可能不太可能;对于'1',也许不是那么不可能。要使用grep,您必须能够使用正则表达式,例如:'^[^;]*;X;' (以隔离出现在第二个字段中的 X 到 X)。
    • @KyleBarbour:您可能可以使用grep -w 'e'(也不要使用单引号),但这不如将其锚定到字段分隔符可靠。另一个字段可能包含“e pluribus unum”(“out of many, one”),它将在;e; 的位置被拾取,因为搜索词不会。你必须决定这是否重要。
    【解决方案2】:

    怎么样:

     sort -t ';' -k 2 test.txt | awk -F';' 'BEGIN{curr="";prev="";flag=0} \
                         NF==8{ prev=curr;
                                curr=$2;
                                if(prev!=curr){flag=1}
                                if(flag!=0 && prev==curr)flag++ ; 
                                if(flag==2)print $0}'
    

    我还尝试了uniq 命令,该命令具有显示重复行“-d”的选项,但无法确定是否可以与字段一起使用。

    【讨论】:

    • 注意不稳定的排序。它可以报告第一行是第二行的重复。
    【解决方案3】:

    一个不分拣的班轮:

    awk '{d[$2][a[$2]++]=$0} END{for (i in a) {if (a[i] > 1) for (j in d[i]) {print d[i][j]}}}'
    

    【讨论】:

      【解决方案4】:

      有一个复杂的awk 脚本。

      awk 'BEGIN { FS=";" } { c[$2]++; l[$2,c[$2]]=$0 } END { for (i in c) { if (c[i] > 1) for (j = 1; j <= c[i]; j++) print l[i,j] } }' file.txt
      

      它的工作原理是为第二个字段中每个值的所有出现次数以及具有该值的行保留一个计数器,然后打印出计数器大于 1 的行。

      $2 的所有实例替换为您需要的任何字段编号,并将末尾的file.txt 替换为您的文件名。

      【讨论】:

        【解决方案5】:

        借用 Hai Vu

        % cat data.txt
        John Thomas;jd;301
        Julie Andrews;jand;109
        Alex Tremble;atrem;415
        John Tomas;jd;302
        Alex Trebe;atrem;416
        

        有一个非常简单的方法(使用 gnu-sort 和 gawk):
        (虽然这会重新排序输出!)
        (警告:如果没有 --stable,排序会重新排序行,因此第二次出现在第一次之前。请注意!)

        cat data.txt | sort -k2,2 -t';' --stable | gawk -F';' '{if ( $2==old ) { print $0 }; old=$2; }'
        

        还有perl方式...

        cat data.txt | perl -e 'while(<>) { @data = split(/;/); if ( defined( $test{$data[1]} ) ) { print $_; } $test{$data[1]} = $_; }'
        

        .

        【讨论】:

          【解决方案6】:

          简单的awk 唯一的方法是根据第 2 列删除唯一行(或根据第 2 列返回重复的行);您可能需要更改为您预期的目标列或多个列的组合$X$Y

          awk -F\; 'NR==FNR{s[$2]++;next} (s[$2]>1)' infile infile
          

          【讨论】:

            【解决方案7】:

            正如@mjv 推测的那样 - awk(或 Perl,或 Python)是更好的选择:

            awk -F';' ' {
                if (assoc[$2]) {          # This field 2 has been seen before
                    if (assoc[$2] != 1) { # The first occurrence has not been printed
                        print assoc[$2];  # Print first line with given $2
                        assoc[$2] = 1;    # Reset array entry so we know we've printed it;
                                          # a full line has 8 fields with semi-colons and
                                          # cannot be confused with 1.
                    }
                    print $0;             # Print this duplicate entry
                }
                else {
                    assoc[$2] = $0;       # Record line in associative array, indexed by
                                          # second field.  
                }
            }' <<!
            a;b;c;d;e;f;g;h
            a;c;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;2;c;d;e;f;g;h
            a;z;c;d;e;f;g;h
            a;q;c;d;e;f;g;h
            a;4;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;x;c;d;e;f;g;h
            a;c;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;q;c;d;e;f;g;h
            a;4;c;d;e;f;g;h
            !
            

            这可行,但它可以稍微重新排序数据 - 因为它会在第二次出现时打印重复行的第一次出现。示例输出为:

            a;1;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;c;c;d;e;f;g;h
            a;c;c;d;e;f;g;h
            a;1;c;d;e;f;g;h
            a;q;c;d;e;f;g;h
            a;q;c;d;e;f;g;h
            a;4;c;d;e;f;g;h
            a;4;c;d;e;f;g;h
            

            awk 脚本的这个变体对测试进行了重新排序,从而使符号更加紧凑。它还明确忽略不包含用分号分隔的 8 个字段的格式错误的数据行。它被打包为一个 shell 脚本,但没有任何选项处理,因此您只能提供要扫描的文件列表(如果没有列出文件,它会读取标准输入)。我删除了脚本中的 Perl-ish 分号; awk 不需要它们。

            #!/bin/sh
            
            awk -F';' '
            NF == 8 {
                if (!assoc[$2]) assoc[$2] = $0
                else if (assoc[$2] != 1)
                {
                    print assoc[$2]
                    assoc[$2] = 1
                    print $0
                }
                else print $0
            }' "$@"
            

            此外,@mjv 评论说,如果输入很大,则可能存在内存问题,因为它会记录关联数组“assoc”中每个不同的字段 2 值。如果输入awk 的数据已排序,我们可以消除这种情况,当然,我们可以使用sort 来确保这一点。这是一个变体脚本,它确实处理巨大的输入(因为sort 会在必要时将数据溢出到磁盘以保存中间结果):

            sort -t';' -k 2,2 "$@" |
            awk -F';' '
            BEGIN { last = ";"; line = "" }
            NF == 8 {
                if ($2 != last)
                {
                    last = $2
                    line = $0
                }
                else if (line != "")
                {
                    print line
                    line = ""
                    print $0
                }
                else print $0;
            }'
            

            这只会保留一行输入的副本。当然,样本数据的输出是按排序顺序给出的。

            【讨论】:

            • 在你的最后一个例子中,最后一行永远不会被打印出来。如果它是倒数第二行的欺骗,那是正确的做法。但如果它是一条独特的线,它应该打印。你需要某种 {END if (line != "") print line} 但我的 AWK 很烂。
            • @LeeMeador:由于问题中给出的目标只是打印重复的行(在它是第 2 列中的重复条目的约束下),所以上面的代码省略最后一个是正确的如果它是唯一的,不是吗?
            【解决方案8】:

            grep 可能会这样做,但我猜您使用 awk(在某些系统上也称为 gawk)会更轻松。

            用于您需要的有效链/脚本取决于一些额外的信息。例如,输入文件是否易于排序,输入有多大(或者说它是巨大的还是流)......

            假设输入是排序的(无论是最初的还是通过排序的管道),awk 脚本看起来像这样:(注意未经测试)

            查看 Jonathan Leffler 或 Hai Vu 提供的解决方案,以了解实现相同预排序要求的方法。

            #!/usr/bin/awk
            # *** Simple AWK script to output duplicate lines found in input ***
            #    Assume input is sorted on fields
            
            BEGIN {
                FS = ";";   #delimiter
                dupCtr = 0;       # number of duplicate _instances_
                dupLinesCtr = 0;  # total number of duplicate lines
            
                firstInSeries = 1;   #used to detect if this is first in series
            
                prevLine = "";
                prevCol2 = "";  # use another string in case empty field is valid
            }
            
            {
              if ($2 == prevCol2) {
                if (firstInSeries == 1) {
                  firstInSeries = 0;
                  dupCtr++;
                  dupLinesCtr++;
                  print prevLine
                }
                dupLinesCtr++;
                print $0
              }
              else
                 firstInSeries = 1
              prevCol2 = $2
              prevLine = $0
            }
            
            END { #optional display of counts etc.
              print "*********"
              print "Total duplicate instances = " iHits "   Total lines = " NR;
            }
            

            【讨论】:

            • 我不相信我有那个工具。当我输入“awk --help”时,我没有收到这样的选项消息
            • 嗯...也许你有傻瓜,这实际上是“更好”(恕我直言)
            • 我不能使用它,因为我没有 awk,任何使用 grep 和/或 uniq 的基于命令行的解决方案?
            • 我不是最新的 linux 发行版,现在,我认为这是大多数(实际上它可能已安装但不在路径中)。如果您愿意/可以,您可以从gnu.org/software/gawk/gawk.html 获得 gawk (GNU awk),它是一个很小的、独立的命令行,将其添加到您的 bin 中,您就可以开展业务了。再说一次......可能有办法用 grep 来实现这一点,但我猜这有点做作。
            • 听起来你有一个非 GNU 版本的 awk——它们不支持 --help。您确实有某个版本的 awk - 如果完全丢失,您将收到一条未找到命令的消息,而不是一条无法识别的选项消息。如果需要,您可以随时安装 awk。
            【解决方案9】:

            我假设您不依赖输入的任何特定顺序(它可能没有在键(第二)字段上预先排序)并且您希望保留输入的顺序输出中的输入行...打印第一行和所有后续行的副本,这些行在第二个字段中包含重复值。

            这是我能用 Python 写出的最快的 sn-p 代码:

                import fileinput
                seen = dict()
                for line in fileinput.input():
                    fields = line.split(';')
                    key = fields[1]
                    if key in seen:
                        if not seen[key][0]:
                            print seen[key][1],
                            seen[key] = (True, seen[key][1])
                        print line,
                    else:
                        seen[key] = (False, line)
            

            fileinput 模块让我们以类似于默认的awk 文件/输入处理的方式处理我们的输入行...或类似于 Perl 的-n 命令行开关的语义。

            从那里我们只需跟踪我们看到的第一行,在第二个字段中使用一个唯一值,以及一个指示我们之前是否打印过这一行的标志。当我们第一次找到一个副本时,我们打印具有该键的第一行,并将其标记为已打印,然后我们打印当前行。对于所有后续重复,我们只打印当前行。显然,对于任何非受骗者,我们只需将其作为条目发布到我们的字典中即可。

            可能有一种更优雅的方式来处理“第一次欺骗”布尔值......但这对我来说是最明显的,不应该带来任何额外的撤销开销。创建一个非常简单的具有自己状态的对象/类(我已被打印)将是一种选择。但我认为这会使代码的整体要点更难理解。

            很明显,这可以在任何支持关联数组(散列、字典、表,无论您喜欢的语言如何称呼它们)的任何脚本或编程语言中完成。此代码与我在此线程中看到的大多数其他示例之间的唯一区别在于我对您的要求所做的假设(您希望保留输入和输出行的相对顺序)。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-10-14
              • 1970-01-01
              相关资源
              最近更新 更多