在GNU/Linux中,将两个文件(作为行集)的笛卡尔积计算出来

20

我该如何使用shell一行命令和常用的GNU工具来像笛卡尔积一样连接两个文件中的行?最简洁、美观和“Linux风格”的方法是什么?

例如,如果我有两个文件:

$ cat file1
a
b
$ cat file2
c
d
e
结果应该是:
a, c
a, d
a, e
b, c
b, d
b, e

哦不,它变成了一场竞赛... - C. Ross
1
@C. Ross,它没有。我有一个明确和表达的标准,不使用perl、python等语言。其余的只是为了可维护性、简洁性和清晰度而进行的常规斗争。 - P Shved
14个回答

17

以下是用Shell脚本实现的方法

while read a; do while read b; do echo "$a, $b"; done < file2; done < file1

尽管这样做可能会非常缓慢。 我想不到任何预编译的逻辑来实现这一点。 为了加快速度,下一步可以在awk/perl中执行上述操作。

awk 'NR==FNR { a[$0]; next } { for (i in a) print i",", $0 }' file1 file2

嗯,使用预编译的逻辑,这个hacky(不太优美但实用的)解决方案怎么样?

paste -d, <(sed -n "$(yes 'p;' | head -n $(wc -l < file2))" file1) \
          <(cat $(yes 'file2' | head -n $(wc -l < file1)))

2
@Pixelbeat:你的第一个版本需要颠倒file1file2的顺序。(也就是说,应该使用done < file2; done < file1才能得到所需的结果。) - Telemachus
3
@Telemachus,顺序无关紧要:如果我说“笛卡尔积”,我真的 是这个意思 - P Shved
@HiteshPatel,我相信这对你有用。唯一需要更改的是将while read a类型的答案正确运行所需添加-r参数,使其变为while read -r a; do while read -r b; do,因为你的内容包含了字面上的反斜杠。(@pixelbeat,您可能需要将这些参数编辑到答案中)。 - Charles Duffy

10

使用join方法,不需要逗号分隔:

$ join -j 2 file1 file2
 a c
 a d
 a e
 b c
 b d
 b e

1
join -j 2 -o '1.1 2.1' -t ', ' file1 file2 - Marcus
@Marcus,也许值得指出的是,如果您将其降级为单个分隔符,即“-t,”,它也将与许多非GNU join实现一起使用。除了OP的限定条件外,更广泛的社区欣赏答案的可移植性。我们不都运行Linux。 :) - ghoti

8
我不会假装这很漂亮,但是...
join -t, -j 9999 -o 2.1,1.1 /tmp/file1 /tmp/file2

(感谢下面的Iwan Aucamp更新)

--join(GNU coreutils)8.4


你可以通过添加“-o'2.1,1.1'”(或者任何你喜欢的顺序)来消除使用cut的需要。 - Iwan Aucamp

7

使用shell机械的方式来做,不使用Perl或Python,如下所示:

while read line1
do
    while read line2
    do echo "$line1, $line2"
    done < file2
done < file1
join 命令有时可以用于这些操作 - 但我不确定它是否能够作为退化情况进行笛卡尔积。
比双重循环更高一级的是:
while read line1
do
    sed "s/^/$line1, /" file2
done < file1

我会选择第一种解决方案,因为它不会让文件看起来有很大的不同。 - P Shved
第一种解决方案可能会慢得多,但它也能够免疫数据中的奇怪字符(例如斜杠)。修复这个问题有点棘手,在那时你开始考虑使用 Perl 或 Python。 - Jonathan Leffler

5

编辑:

DVK的尝试启发了我使用eval进行此操作:

script='1{x;d};${H;x;s/\n/\,/g;p;q};H'
eval "echo {$(sed -n $script file1)}\,\ {$(sed -n $script file2)}$'\n'"|sed 's/^ //'

或者一个更简单的 sed 脚本:

script=':a;N;${s/\n/,/g;b};ba'

如果不使用-n开关,则会使用它。

这将产生:

a, c
a, d
a, e
b, c
b, d
b, e

Translated answer:

在Bash中,你可以这样做。它不会从文件中读取,但这是一个很好的技巧:

$ echo {a,b}\,\ {c,d,e}$'\n'
a, c
 a, d
 a, e
 b, c
 b, d
 b, e

更简单地说:
$ echo {a,b}{c,d,e}
ac ad ae bc bd be

不错。但我肯定不想维护这个脚本。 :) - ghostdog74
真是令人愉悦,但难以维护。 :) - P Shved

3
一种通用的递归BASH函数可能如下所示:
foreachline() {

    _foreachline() {

        if [ $#  -lt 2 ]; then
            printf "$1\n"
            return
        fi

        local prefix=$1
        local file=$2
        shift 2

        while read line; do
            _foreachline "$prefix$line, " $*
        done <$file
    }

    _foreachline "" $*
}

foreachline file1 file2 file3

祝好。


2
这个解决方案在这些解决方案中是独一无二的,因为它解决了任意笛卡尔积操作集的更一般情况。 - Brian Chrisman
1
使用$*而不是"$@"是不幸的,因为这意味着任何作为参数的"*"都将被替换为文件名列表。 - Charles Duffy

2

编辑:哎呀...对不起,我以为这是标记了Python的...

如果您有Python 2.6:

from itertools import product
print('\n'.join((', '.join(elt) for elt in (product(*((line.strip() for line in fh) for fh in (open('file1','r'), open('file2','r'))))))))

a, c
a, d
a, e
b, c
b, d
b, e

如果你使用的是Python版本在2.6之前:

def product(*args, **kwds):
    '''
    Source: http://docs.python.org/library/itertools.html#itertools.product
    '''
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
    pools = map(tuple, args) * kwds.get('repeat', 1)
    result = [[]]
    for pool in pools:
        result = [x+[y] for x in result for y in pool]
    for prod in result:
        yield tuple(prod)
print('\n'.join((', '.join(elt) for elt in (product(*((line.strip() for line in fh) for fh in (open('file1','r'), open('file2','r'))))))))

那个方法可以行,但是Python不是我一直在寻找的。 - P Shved

2

使用joinawk和进程替换的解决方案:

join <(xargs -I_ echo 1 _ < setA) <(xargs -I_ echo 1 _ < setB)
  | awk '{ printf("%s, %s\n", $2, $3) }'

文件“a”的内容是什么?它们中的一个应该是另一个文件吗?AWK 可能可以被 cut -f2- -d' ' 替换。 - Dennis Williamson
"a" 文件包含了这个集合。如果需要的话,它们可能是不同的。我来纠正一下! - yassin
@Dennis,cut可能更好,因为它可以处理setB中包含空格的行。 - P Shved

2

解决方案1:

perl -e '{use File::Slurp; @f1 = read_file("file1"); @f2 = read_file("file2"); map { chomp; $v1 = $_; map { print "$v1,$_"; } @f2 } @f1;}'

这个命令使用了Perl脚本语言,通过File::Slurp模块读取文件内容,并将两个文件的每一行进行组合输出。需要注意的是,保留了HTML标签,同时也保留了原文中的代码格式。

你为什么在这里使用了 map?那些应该是 for 循环。 - user181548
@Kinopiko:你不是刚在另一个帖子中抱怨“语言警察”吗? - Telemachus
我最喜欢使用的除了地图之外就是正则表达式了。 :) - DVK
@Telemachus:如果你打不败他们,就加入他们。 - user181548
编程警察就在这里:编程警察即将到来并抓住你! :-) - P Shved
你有为此获得_徽章_吗? :) - DVK

1
awk 'FNR==NR{ a[++d]=$1; next}
{
  for ( i=1;i<=d;i++){
    print $1","a[i]
  }
}' file2 file1

# ./shell.sh
a,c
a,d
a,e
b,c
b,d
b,e

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