在Unix上连接文本文件中的多个字段

时间:2010-04-12 02:30:00

标签: linux bash unix join

我该怎么做?

File1 如下所示:

foo 1 scaf 3 
bar 2 scaf 3.3

File2 如下所示:

foo 1 scaf 4.5
foo 1 boo 2.3
bar 2 scaf 1.00

我想要做的是找到在 File1 File2 中共同出现的行 当字段 1,2和3 相同时。

有办法吗?

11 个答案:

答案 0 :(得分:12)

以下是正确的答案(就使用标准的 GNU coreutils 工具而言,而不是在 perl / awk 中编写自定义脚本它)。

$ join -j1 -o1.2,1.3,1.4,1.5,2.5 <(<file1 awk '{print $1"-"$2"-"$3" "$0}' | sort -k1,1) <(<file2 awk '{print $1"-"$2"-"$3" "$0}' | sort -k1,1)
bar 2 scaf 3.3 1.00
foo 1 scaf 3 4.5

好的,它是如何运作的:

  1. 首先,我们将使用一个可以合并两行的好工具joinjoin有两个要求:

    • 我们只能通过一个字段加入
    • 两个文件必须按键列排序
  2. 我们需要在输入文件中生成 keys ,为此我们使用简单的awk脚本:

    $ cat file1
    foo 1 scaf 3
    bar 2 scaf 3.3    
    
    $ <file1 awk '{print $1"-"$2"-"$3" "$0}'
    foo-1-scaf foo 1 scaf 3
    bar-2-scaf bar 2 scaf 3.3
    

    你看,我们在第一栏添加了一些关键词,比如“ foo-1-scaf ”。 我们对 file2 做同样的事情。 BTW。 <file awk只是写awk filecat file | awk的奇特方式。

    我们还应该通过键排序我们的文件,在我们的例子中这是第1列,所以我们添加 在命令的末尾| sort -k1,1排序按第1列到第1列的文字)

  3. 此时我们可以生成文件 file1.with.key file2.with.key 并加入它们, 但假设这些文件很大,我们不想将它们复制到文件系统上。相反,我们可以使用名为bash process substitution的东西来生成命名管道的输出(这将避免任何 不必要的中间文件创建)。欲了解更多信息,请阅读提供的链接。

    我们的目标语法是:join <( some command ) <(some other command)

  4. 最后一件事是解释花哨的连接参数:-j1 -o1.2,1.3,1.4,1.5,2.5

    • -j1 - 在第1列(在两个文件中)按键加入
    • -o - 仅输出字段1.2(第一个文件字段2),1.3(第一个文件第3列)等。

      这样我们就加入了行,但join只输出必要的列。

  5. 从这篇文章中吸取的教训应该是:

    • 你应该掌握 coreutils 包,这些工具组合起来非常强大,你几乎永远不需要编写自定义程序来处理这些情况,
    • core utils工具也非常快速且经过严格测试,因此它们始终是最佳选择。

答案 1 :(得分:9)

join命令很难使用,只能连接一列

广泛的实验加上对手册页的仔细审查表明你不能直接加入多个列 - 而且我所有的加入实例都很有趣,只使用一个连接列。

因此,任何解决方案都需要以某种方式将要连接的列连接成一列。标准连接命令还要求其输入处于正确的排序顺序 - 在GNU连接(info coreutils join)中有一条关于它并不总是需要排序数据的注释:

  

但是,作为GNU扩展,如果输入没有无法使用的行   排序顺序可以是任何将两个字段视为相等的顺序,如果和   只有当上述排序比较认为它们相等时才会发生。

使用给定文件执行此操作的一种可能方法是:

awk '{printf("%s:%s:%s %s %s %s %s\n", $1, $2, $3, $1, $2, $3, $4);}' file1 |
sort > sort1
awk '{printf("%s:%s:%s %s %s %s %s\n", $1, $2, $3, $1, $2, $3, $4);}' file2 |
sort > sort2
join -1 1 -2 1 -o 1.2,1.3,1.4,1.5,2.5 sort1 sort2

这会在开始时创建一个复合排序字段,使用':'分隔子字段,然后对文件进行排序 - 对于两个文件中的每一个。然后join命令连接两个复合字段,但只打印出非复合(非连接)字段。

输出结果为:

bar 2 scaf 3.3 1.00
foo 1 scaf 3 4.5

尝试联接做失败的尝试

    join -1 1 -2 1 -1 2 -2 2 -1 3 -2 3 -o 1.1,1.2,1.3,1.4,2.4 file1 file2

在MacOS X 10.6.3上,这给出了:

$ cat file1
foo 1 scaf 3 
bar 2 scaf 3.3
$ cat file2
foo 1 scaf 4.5
foo 1 boo 2.3
bar 2 scaf 1.00
$ join -1 1 -2 1 -1 2 -2 2 -1 3 -2 3 -o 1.1,1.2,1.3,1.4,2.4 file1 file2
foo 1 scaf 3 4.5 
bar 2 scaf 3.3 4.5 
$

这是加入第3场(仅限) - 这不是想要的。

您需要确保输入文件的排序顺序正确。

答案 2 :(得分:4)

你可以试试这个

awk '{
 o1=$1;o2=$2;o3=$3
 $1=$2=$3="";gsub(" +","")
 _[o1 FS o2 FS o3]=_[o1 FS o2 FS o3] FS $0
}
END{ for(i in _) print i,_[i] }' file1 file2

输出

$ ./shell.sh
foo 1 scaf  3 4.5
bar 2 scaf  3.3 1.00
foo 1 boo  2.3

如果你想省略不常见的行

awk 'FNR==NR{
 s=""
 for(i=4;i<=NF;i++){ s=s FS $i }
 _[$1$2$3] = s
 next
}
{
  printf $1 FS $2 FS $3 FS
  for(o=4;o<NF;o++){
   printf $i" "
  }
  printf $NF FS _[$1$2$3]"\n"
 } ' file2 file1

输出

$ ./shell.sh
foo 1 scaf 3  4.5
bar 2 scaf 3.3  1.00

答案 3 :(得分:3)

将前三个字段与awk结合起来可能最简单:

awk '{print $1 "_" $2 "_" $3 " " $4}' filename

然后您可以在“字段1”

上正常使用join

答案 4 :(得分:2)

怎么样:

cat file1 file2
    | awk '{print $1" "$2" "$3}'
    | sort
    | uniq -c
    | grep -v '^ *1 '
    | awk '{print $2" "$3" "$4}'

这假设您不太担心字段之间的空白区域(换句话说,三个制表符和一个空格与空格和7个制表符没有区别)。当您谈论文本文件中的字段时,通常会出现这种情况。

它做的是输出两个文件,剥离最后一个字段(因为你在比较方面不关心那个)。它是相似的线相邻的排序然后将它们统一起来(用一个副本和一个计数替换每组相邻的相同线)。

然后摆脱所有那些具有一个计数(没有重复)的人,并打印出每个被剥离的计数。这为您提供了重复行的“键”,然后您可以使用另一个awk迭代来根据需要在文件中找到这些键。

如果两个相同的密钥只在一个文件中,因为文件在早期组合,按预期工作。换句话说,如果您在file1中有重复的密钥,但在file2中却没有,那将是误报。

然后,我能想到的唯一真正的解决方案是检查file2 file1中每一行的解决方案,尽管我确信其他人可能会提出更聪明的解决方案。


而且,对于那些喜欢一点受虐狂的人来说,这是上述不太有效的解决方案:

cat file1
    | sed
        -e 's/ [^ ]*$/ "/'
        -e 's/ /  */g'
        -e 's/^/grep "^/'
        -e 's/$/ file2 | awk "{print \\$1\\" \\"\\$2\\" \\"\\$3}"/'
    >xx99
bash xx99
rm xx99

这个构造一个单独的脚本文件来完成工作。对于file1中的每一行,它会在脚本中创建一行以在file2中查找该行。如果您想了解它的工作原理,请在删除之前先查看xx99

而且,在这一篇中,空格确实重要,所以如果它不适用于file1file2之间空格不同的行,请不要感到惊讶(尽管如此,与大多数空格一样)可怕的“脚本,可以通过管道中的另一个链接修复”。这里更多的是你可以为快速工作创造的可怕事物的例子。

这是我会为生产质量代码做什么,但只要你在The Daily WTF发现它之前销毁它的所有证据,它就可以一次性使用: - )

答案 5 :(得分:2)

以下是在Perl中执行此操作的方法:

#!/usr/local/bin/perl
use warnings;
use strict;
open my $file1, "<", "file1" or die $!;
my %file1keys;
while (<$file1>) {
    my @keys = split /\s+/, $_;
    next unless @keys;
    $file1keys{$keys[0]}{$keys[1]}{$keys[2]} = [$., $_];
}
close $file1 or die $!;
open my $file2, "<", "file2" or die $!;
while (<$file2>) {
    my @keys = split /\s+/, $_;
    next unless @keys;
    if (my $found = $file1keys{$keys[0]}{$keys[1]}{$keys[2]}) {
        print "Keys occur at file1:$found->[0] and file2:$..\n";
    }
}
close $file2 or die $!;

答案 6 :(得分:2)

简单方法(没有 awk 加入 sed perl ),使用软件工具{{ 1}},cutgrep

sort

输出(不打印不匹配的行):

cut -d ' ' -f1-3 File1 | grep -h -f - File1 File2 | sort -t ' ' -k 1,2g

工作原理......

  1. bar 2 scaf 1.00 bar 2 scaf 3.3 foo 1 scaf 3 foo 1 scaf 4.5 列出了要搜索的所有行。
  2. cut的{​​{1}}开关输入来自grep的行,并搜索 File1 File2
  3. -f -不是必需的,但可以让数据更易于阅读。
  4. cut的简明结果:

    sort

    输出:

    datamash

    如果 File1 很大并且有点多余,那么添加cut -d ' ' -f1-3 File1 | grep -h -f - File1 File2 | \ datamash -t ' ' -s -g1,2,3 collapse 4 可以加快速度:

    bar 2 scaf 3.3,1.00
    foo 1 scaf 3,4.5
    

答案 7 :(得分:1)

我曾经合作过的一位教授创建了一组perl脚本,可以在面向列的平面文本文件上执行大量类似数据库的操作。它被称为Fsdb。它绝对可以做到这一点,特别值得研究的是,这不仅仅是一次性需求(因此你不会经常编写自定义脚本)。

答案 8 :(得分:1)

与Jonathan Leffler提供的类似解决方案。

使用不同的分隔符创建2个临时排序文件,这些分隔符在第一个字段中包含匹配的列。 然后在第一个字段上加入临时文件,并输出第二个字段。

main

答案 9 :(得分:1)

使用datamash&#39> 崩溃操作,加上一些整容sorttr

cat File* | datamash -t ' ' -s -g1,2,3  collapse 4 | sort -g -k2 | tr ',' ' '

输出(公共线有第5个字段,不常见的线没有):

foo 1 boo 2.3
foo 1 scaf 3 4.5
bar 2 scaf 3.3 1.00

答案 10 :(得分:0)

OP 没有显示预期的输出,所以 idk 是否正是所需的输出,但这是解决问题的方法:

$ awk '
    { key=$1 FS $2 FS $3 }
    NR==FNR { val[key]=$4; next }
    key in val {print $0, val[key] }
' file1 file2
foo 1 scaf 4.5 3
bar 2 scaf 1.00 3.3