3

我在一个目录中有输入文件。所有输入文件都具有相同的格式,我想将这些输入文件中的某些列连接到一个输出文件中。

例如:

在文件 1

Adam    0.5 a1
Bills   0.7 b1
Carol   0.8 c1
Dean    0.4 d1

在文件 2

Adam    0.4 a2
Carol   0.8 c2
Evan    0.9 e2

在文件 3

Bills   0.6 b3
Carol   0.7 c3
Evan    0.1 e3

我想通过使用第一列作为键来加入所有输入文件的第三列。所以输出可能看起来像

Adam    a1  a2  NA
Bills   b1  NA  b3
Carol   c1  c2  c3
Dean    d1  NA  NA
Evan    NA  e2  e3

由于输入文件的数量不同,输出中的列数也不同。输入文件的数量至少为 200 个,最大为 10,000 个。

我找不到一种简单的方法来使用“for”、“awk”、“join”、“cut”来解决这个问题。是的,我可以编写一个 Python 或 Perl 脚本来解决这个问题,但我想知道这是否可以单独使用 bash 脚本来完成?

附言。在提出这个问题之前,我试图寻找解决方案,但找不到。如果已经问过这种问题,请指出答案。

4

2 回答 2

4

您可以通过组合两个joins 来做到这一点。

$ join -o '0,1.3,2.3' -a1 -a2 -e 'NA' file1 file2
Adam a1 a2
Bills b1 NA
Carol c1 c2
Dean d1 NA
Evan NA e2

首先将前两个文件连接在一起,-a1 -a2用于确保仍然打印仅存在于一个文件中的行。-o '0,1.3,2.3'控制输出哪些字段并将-e 'NA'缺少的字段替换为NA.

$ join -o '0,1.3,2.3' -a1 -a2 -e 'NA' file1 file2 | join -o '0,1.2,1.3,2.3' -a1 -a2 -e 'NA' - file3
Adam a1 a2 NA
Bills b1 NA b3
Carol c1 c2 c3
Dean d1 NA NA
Evan NA e2 e3

然后将其通过管道join传输到另一个加入第三个文件的文件。这里的技巧是-作为第一个文件名传入,它告诉join使用 stdin 作为第一个文件。


对于任意数量的文件,这里有一个递归应用这个想法的脚本。

#!/bin/bash

join_all() {
    local file=$1
    shift

    awk '{print $1, $3}' "$file" | {
        if (($# > 0)); then
            join2 - <(join_all "$@") $(($# + 1))
        else
            cat
        fi
    }
}

join2() {
    local file1=$1
    local file2=$2
    local count=$3

    local fields=$(eval echo 2.{2..$count})
    join -a1 -a2 -e 'NA' -o "0 1.2 $fields" "$file1" "$file2"
}

join_all "$@"

示例用法:

$ ./joinall file1
Adam a1
Bills b1
Carol c1
Dean d1

$ ./joinall file1 file2
Adam a1 a2
Bills b1 NA
Carol c1 c2
Dean d1 NA
Evan NA e2

$ ./joinall file1 file2 file3
Adam a1 a2 NA
Bills b1 NA b3
Carol c1 c2 c3
Dean d1 NA NA
Evan NA e2 e3
于 2013-08-27T14:01:07.643 回答
2

要在 中加入大量这些文件bash,您需要join谨慎使用该命令。(请参阅bash脚本以从多个 CSV 文件中查找匹配的行加入目录中的所有文件以获得一些想法。)

一个问题是join一次只能连接两个文件。由于原始数据文件有一个不需要的列,而join中间数据有可变数量的列(所有这些都是需要的),因此您必须非常小心地处理 200 个文件。头脑简单的线性方法会起作用——你必须执行 199 个join命令。如果您尝试使用对数方法,则不一定会执行较少的命令,因此您不妨使用线性方法。

我将假设脚本的参数是要连接的文件的名称,这些文件列在要连接的序列中。我还将假设所有数据文件都是预先排序的。您可以相当轻松地将排序构建到脚本中。使用bash, 使用进程替换 代替命令中<(sort "$file")的just ;大多数其他外壳程序将需要在命令中指示“标准输入”的位置(并且该技术也可以正常工作)。这被打包为一个脚本,该脚本作为命令行参数传递要加入的文件列表,因此是符号。"$file"joinsort "$file" | join ... - >$tmp2-joinbashfor file in "$@"

old=/dev/null
ocount=1
ofields=""

tmp1=tmp.$$.1
tmp2=tmp.$$.2
trap "rm -f $tmp1 $tmp2; exit 1" 0 1 2 3 13 15

for file in "$@"
do
    join -e NA -a 1 -a 2 -o "0 $ofields 2.3" "$old" "$file" > $tmp2
    ofields="$ofields 1.$((++ocount))"
    mv $tmp2 $tmp1
    old=$tmp1
    # echo "== $file"
    # cat $old
done
mv $tmp1 output.txt

trap 0

代码捕获中断和相关信号并删除临时文件并以错误状态退出。它使用 shell 算法来建立输出列的列表;0表示名称(连接列),并且2.3是第二个(新)文件的第三列。该$ofields变量包含1.2 1.3 1.4 ...指定前一个文件中的非连接列的数字。

给定数据的示例输出:

Adam a1 a2 NA
Bills b1 NA b3
Carol c1 c2 c3
Dean d1 NA NA
Evan NA e2 e3
于 2013-08-27T15:08:51.240 回答