当标题与多个文件匹配时,连接列值

时间:2016-09-24 03:00:50

标签: perl unix join awk sed

可以有n列,但列数和标题名称在所有3个文件中保持相同。文件是制表符分隔符。

A.TXT

Name  9/1   9/2
X   1   7
y   2   8
z   3   9
a   4   10
b   5   11
c   6   12

b.xt

Name  9/1   9/2
X   13  19
y   14  20
z   15  21
a   16  22
b   17  23
c   18  24

c.txt

Name  9/1   9/2
X   25  31
y   26  32
z   27  33
a   28  34
b   29  35
c   30  36

必需的输出

Name  9/1   9/2
X   1/13/25 7/19/31
y   2/14/26 8/20/32
z   3/15/27 9/21/33
a   4/16/28 10/22/34
b   5/17/29 11/23/35
c   6/16/30 12/24/36

我想在匹配标题的基础上合并所有三个文件,如果行中的值相同,则打印一个值 如果不同,则连接该列下所有三个文件的值。比如文件a.txt,b.txt,c.txt, 第2行在列名“NAME”下具有相同的值,因此仅打印X但列“9/1”具有不同的值,因此打印一个标题“9/1”并在其下打印来自三个文件的所有值,例如25年1月13日

尝试下面

join <(sort a.txt) <(sort b.txt) <(sort c.txt) >out.txt

但它正在打印文件中的所有行a然后是起始文件b然后是c 同样的事情

awk 'FNR==NR{a[$1]=$2;next} ($1 in a){ print $0, a[$1]}' a.txt b.txt c.txt

3 个答案:

答案 0 :(得分:2)

输出顺序是Gnu awk默认值:

$ cat > combine.txt 
FNR==NR {
    a[$1]=$2                                # gather the 2 dates in variables
    b[$1]=$3
    next
} 
FNR>1 && ($1 in a) {
    a[$1]=a[$1] "/" $2
    b[$1]=b[$1] "/" $3
} 
END {                                       # print built up variables in the end
    # PROCINFO["sorted_in"]="@ind_str_asc"  # output order control with this
                                            # in Gnu awk 
    for(i in a) print i, a[i], b[i]
}

测试它:

$ awk -f comnine.awk a.txt b.txt c.txt
y 2/14/26 8/20/32
z 3/15/27 9/21/33
a 4/16/28 10/22/34
b 5/17/29 11/23/35
c 6/18/30 12/24/36
X 1/13/25 7/19/31
Name 9/1 9/2

Gnu awk中的修订版本应该支持超过2个数据字段。我只测试了您提供的数据。让我知道它是否有效,我删除了原有的限制版本。

FNR==NR {
    for(i=2;i<=NF;i++)
        a[$1][i]=$i
    next
}
FNR>1 && ($1 in a) {
    for(i=2;i<=NF;i++)
        a[$1][i]=a[$1][i] "/" $i
}
END {                                       # print built up variables in the end                                                                              
    # PROCINFO["sorted_in"]="@ind_str_asc"  # output order control with this    
                                            # in Gnu awk                        
    for(i in a) {
        printf "%s", i OFS
        for(j=2;j<=NF;j++)
            printf "%s", a[i][j] (j<NF?OFS:ORS)
    }
}

由于你有tagget join,我还是决定尝试一下:

$ join -o 1.1 1.2 2.2 2.4 1.3 2.3 2.5 a.txt <(join b.txt c.txt) | sed 's/ /\//g2; s/\// /3'
Name 9/1/9 1/9/1/9/2/9/2/9/2
X 1/13/25 7/19/31
y 2/14/26 8/20/32
z 3/15/27 9/21/33
a 4/16/28 10/22/34
b 5/17/29 11/23/35
c 6/18/30 12/24/36

即。首先加入b.txtc.txt,然后将结果加入a.txt。使用sed替换某些空格时,/用于输出控制。 man join显示--header转换,但它不想与我合作。

答案 1 :(得分:1)

这一次从所有文件中读取一行,并为每个文件和列连接相同的字段。假设所有文件中每行的第一个字段相同。 (提交已排序的文件。)

use warnings 'all';
use strict;

# Implicit filehandles. Use readline() to read from them, not <>
my @fh = map { open my $fh, '<', $_  or die "Can't open $_: $!"; $fh } @ARGV;

while (1) 
{ 
    # Read a line from all files. Exit loop if any is undefined
    my @line = map { scalar readline $_ } @fh;
    last if  grep { not defined $line[$_] } 0..$#line;
    # Print first line (header) from one file, skip further processing
    if ($. == 1) { 
        print $line[0];
        next;
    }   

    # Get the first column from one file, assumed the same for all  
    my @out_line = (split ' ', $line[0])[0];
    # Join same column from all files with '/', for all columns
    for my $i (1..$#line) {
        push @out_line, join '/', map { (split)[$i] } @line;
    }   
    print "@out_line\n";
}

在给定排序的输入文件的情况下,使用script.pl a.txt b.txt c.txt生成所需的输出。

评论。如果更全面的解释会有所帮助,请告诉我。

  • 假设第一列相同。一个文件耗尽后立即退出

  • 使用“隐式文件句柄” - 通过readline而不是<> 运算符从中读取。请参阅in perlfaq5

  • scalar readline $_强制标量上下文,以便读取一行

  • 留下换行符。要删除它,请在chomp(@lines)

  • 之后添加last if ...
  • 前两行可用于构成while (...)条件

原帖,浓缩。留在这里是为了讨论join

的可能用途

这使您几乎可以使用join,并使用Perl完成工作。

首先,join当时只占用两个文件。因此,您可以使用前两个运行它,然后使用生成的文件和最后一个文件运行它。我们还需要告诉它如何格式化输出。这是通过其-o选项完成的, 这允许我们将输出行的元素列为FileN.fieldN, ...

您需要的是:1.1 1.2 2.2 1.3 2.3 - 文件1中的字段1,2,然后是2中的2,等等< / p>

join -o 1.1,1.2,2.2,1.3,2.3 a.txt b.txt  > ab.txt

文件ab.txt具有不同的格式,因此行的-o格式会在下一步中发生变化

join -o 1.1,1.2,1.3,2.2,1.4,1.5,2.3 ab.txt c.txt  > abc.txt

这有两个故障 - 标题已连接,字段与空格合并。用Perl单行修复。

收集bash脚本。工作草图,以script.sh a.txt b.txt c.txt

运行
#!/bin/bash

join -o 1.1,1.2,2.2,1.3,2.3         $1     $2  > tmp.$$
join -o 1.1,1.2,1.3,2.2,1.4,1.5,2.3 tmp.$$ $3  > abc.txt

# Remove extra fields in the header    
perl -i -wpe '$.==1 && s{(\d/\d) \1 \1}{$1}g'  abc.txt

# Replace space with `/` between every three numbers
perl -i -wpe 's{(\d+) (\d+) (\d+)}{$1/$2/$3}g' abc.txt

rm -f tmp.$$

请更仔细地选择临时名称并添加错误检查。输出abc.txt

Name 9/1 9/2
X 1/13/25 7/19/31
y 2/14/26 8/20/32
...

答案 2 :(得分:1)

这可能适合你(GNU sed&amp; bash):

对于给出的示例(两列):

sed -sr '1d;s#(.*\t)(.*)\t(.*)#/^\1/s@(\t.*)(\t.*)@\\1/\2\\2/\3@#' file{b,c} |
sed -rf - filea

这将从第一个以外的文件构建一个sed脚本,该脚本插入与每行的键匹配的值。除了在构建脚本时以正确的顺序放置文件之外,不需要排序。然后管道创建的脚本并针对第一个文件运行以获得所需的结果。

N.B。 -s选项允许每个文件的标题行与每个子文件无关,父文件将不匹配,并且将保持不变。

然而,问题允许使用数量不详的列,这需要一个类似但更复杂的解决方案:

sed -sr '1d;s#[^\t]+#/^&/{#;s#\t([^\t]+)#s@\\t([^\\t]+)@\\n\\1/\1@;#g;s#$#y/\\n/\\t/;}#' file{b,c}|
sed -rf - filea

在此解决方案中,父/子文件中的每个选项卡都需要替换命令(如前面的解决方案中,后向引用已命名,因此限制为9)。通过删除选项卡并将其替换为换行符来关闭每个替换,从而将后向引用减少为仅一个,然后通过再次将换行符替换为制表符来替换换行符。