使用Shell删除列中的重复字符串

11

我有一个文件,其中有两列,以制表符分隔,如下所示:

OG0000000   PF03169,PF03169,PF03169,MAC1_004431-T1,
OG0000002   PF07690,PF00083,PF00083,PF07690,PF00083,
OG0000003   MAC1_000127-T1,
OG0000004   PF13246,PF00689,PF00690,
OG0000005   PF00012,PF01061,PF12697,PF00012,

我只想删除第二列中的重复字符串,同时不改变第一列中的任何内容,这样我的最终输出看起来像这样:

OG0000000   PF03169,MAC1_004431-T1,
OG0000002   PF07690,PF00083,
OG0000003   MAC1_000127-T1,
OG0000004   PF13246,PF00689,PF00690,
OG0000005   PF00012,PF01061,PF12697,

我试着用awk开始处理这个。

awk 'BEGIN{RS=ORS=","} !seen[$0]++' file.txt

但我的输出看起来像这样,在重复的字符串首次出现时仍然存在一些重复。

OG0000000   PF03169,PF03169,MAC1_004431-T1,
OG0000002   PF07690,PF00083,PF07690,
OG0000003   MAC1_000127-T1,
OG0000004   PF13246,PF00689,PF00690,
OG0000005   PF00012,PF01061,PF12697,PF00012,

我意识到问题是因为awk获取的第一行是直到第一个逗号之前的所有内容,但我对awk命令还不熟悉,无法想出如何在不弄乱第一列的情况下解决此问题。先提前感谢!


1
$0 表示整行。因此,您可以在变量 seen 中记录唯一的整行,同时只关注第二列的部分。 - user1934428
我认为您也没有指定以下情况:第1行有OG1 A,B,C,B,第2行有OG2 B,D。因为_B_已经出现在第1行中,所以第2行中的_B_也应该被删除吗? - user1934428
6个回答

11
这个awk应该适合你的需求:
awk -F '[\t,]' '
{
   printf "%s", $1 "\t"
   for (i=2; i<=NF; ++i) {
      if (!seen[$i]++)
         printf "%s,", $i
   }
   print ""
   delete seen
}' file

OG0000000   PF03169,MAC1_004431-T1,
OG0000002   PF07690,PF00083,
OG0000003   MAC1_000127-T1,
OG0000004   PF13246,PF00689,PF00690,
OG0000005   PF00012,PF01061,PF12697,

PS:按照期望的输出结果,此解决方案在每行末尾也显示了逗号。


1
该死,复合FS - 又短了! - David C. Rankin
2
你不能依赖通常的(i<NF ? "," : ORS)习惯用法,因为如果$NF是重复的,那么你将不会为该行打印ORS。 - Ed Morton
1
是的,这是一个很好的观点,Ed。我已经注意到 OP 无论如何都希望有一个尾部斜杠,所以现在让它变得简单了。 - anubhava
我没有看到任何其他的注释或评论表明需要一个尾随的,,所以我现在会保留我的评论,但如果您更新您的答案并提到它,我会删除它。 - Ed Morton
Ed:根据问题中显示的预期输出,所有行都有尾随逗号。我在我的答案中做了记录。 - anubhava

8

采用相同的方法将$2分割成一个数组,并保留一个单独的计数器来记录非重复值的位置,也可以这样做:

awk '
  { 
    printf "%s\t", $1
    delete seen
    n = split($2,arr,",")
    pos = 0
    for (i=1;i<=n;i++) { 
      if (! (arr[i] in seen)) { 
        printf "%s%s", pos ? "," : "", arr[i]
        seen[arr[i]]=1
        pos++ 
      }
    }
    print ""
  }
' file.txt

示例输出

在您的输入文件 file.txt 中,输出结果如下:

OG0000000       PF03169,MAC1_004431-T1,
OG0000002       PF07690,PF00083,
OG0000003       MAC1_000127-T1,
OG0000004       PF13246,PF00689,PF00690,
OG0000005       PF00012,PF01061,PF12697,

1
if (! (arr[i] in seen)) { foo; seen[arr[i]]=1 } can be done a bit more concisely and idiomatically with if (!seen[arr[i]]++) { foo } - Ed Morton
++有很多好的解决方案 - anubhava

6

根据您现有的示例和尝试,请尝试以下 awk 代码。我们不需要设置记录分隔符 RS 和输出记录分隔符 ORS ,因为在此要求中我们不需要设置它们。将 FS 和 OFS 设置为 , 并相应地打印字段。

awk '
BEGIN{ FS=","; OFS="\t" }
{
  val=""
  delete arr
  num=split($2,arr,",")
  for(i=1;i<=num;i++){
   if(!arr[$i]++){
      val=(val?val ",":"") $i
   }
  }
  print $1,val
}
' Input_file

1
if(!arr[$i]++){的上下文中使用的数组惯用命名为seen[],而不是arr[] - Ed Morton
2
等一下 - 当 $2 已经通过 FS 分割时,您不能再次通过 FS 分割它(当 FS, 时同样适用于通过 , 分割)。 - Ed Morton

6

这可能适用于您(GNU sed):

sed -E ':a;s/(\s+.*(\b\S+,).*)\2/\1/;ta' file

遍历一行文本,删除任何在空格之后重复的字符串。


++ 很好,一个更短的gnu-sed - anubhava
1
非常好,不确定但是在\2之前是否漏掉了另一个\b?请参见此演示与例如那个的比较。 - bobble bubble

5

使用GNU的sed

$ sed -E ':a;s/([^ \t]*[ \t]+)?(([[:alnum:]]+,).*)\3/\1\2/;ta' input_file
OG0000000   PF03169,MAC1_004431-T1,
OG0000002   PF07690,PF00083,
OG0000003   MAC1_000127-T1,
OG0000004   PF13246,PF00689,PF00690,
OG0000005   PF00012,PF01061,PF12697,

1
这纯粹是 sed 的创意,值得一提。我用过 sed 很长时间了,也欣赏 ta 在成功替换时的重复使用,但我还在为如何识别重复项以及如何使用第一个反向引用而感到困惑。(我会搞定的,只需要再琢磨一下......)最大的问题是如果重复项不相邻会怎样? - David C. Rankin
@DavidC.Rankin 让第一个反向引用变为可选,这样第二个反向引用就可以循环。在第二个反向引用中嵌套第三个括号,然后使用贪婪正则表达式来删除第三个匹配的最后一次出现,返回循环中第二个括号内的所有内容。当然,它也会处理同一行上的非相邻重复项,就像提供的示例一样。 - HatLess
1
我从宏观角度上有点理解了,但我必须告诉你,这确实是 sed 的一个令人印象深刻的用法。干得好。(注:当我说“有点”时,我的意思是我已经理清了流程并认识到了反向引用嵌套——但远远没有消化到我有一个“啊哈!”时刻的程度 :) - David C. Rankin
1
++ 但我认为这也可以修改为 POSIX sed - anubhava

2

这是一段 Ruby 代码:

ruby -ane 'puts "#{$F[0]}\t#{$F[1].split(/(?<=.),(?=.)/).uniq.join(",")}"' file
OG0000000   PF03169,MAC1_004431-T1,
OG0000002   PF07690,PF00083,PF00083,
OG0000003   MAC1_000127-T1,
OG0000004   PF13246,PF00689,PF00690,
OG0000005   PF00012,PF01061,PF12697,PF00012,

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接