总是乐于助人的GNU datmash 来救援!
$ sort -k1,2 -u input.txt |
awk -v OFS="\t" '$2 < $1 { tmp = $1; $1 = $2; $2 = tmp } { print $1, $2, $3 }' |
sort -k1,2 |
datamash groupby 1,2 collapse 3 |
tr ',' ' '
ABC MNH 1 4
LOI PUQ 5
LOI QWE 3
LOI UHR 2 8
分解一下,这个:
- 根据前两列对输入文件进行排序并删除重复项。
- 如果第二列小于第一列,则交换两列(因此
MNH ABC 6 变为 ABC MNH 6),并输出制表符分隔的列(这是 datamash 默认使用的)。
- 对所有转换后的行进行排序(但这次保留重复)。
- 使用
datamash 为所有重复的前两列生成一行,并以逗号分隔的第三列值列表作为输出的第三列(如ABC MNH 1,4)
- 将这些逗号变成空格。
大多数内存效率高的解决方案都需要对数据进行排序,虽然 sort 程序非常擅长这样做,但它仍会使用一堆临时文件,因此您需要 2-3 个左右TB 的可用磁盘空间。
如果您要使用相同的数据做很多事情,那么作为管道的第一步,可能值得对其进行一次排序并重复使用该文件,而不是每次都对其进行排序:
$ sort -k1,2 -u input.txt > unique_sorted.txt
$ awk ... unique_sorted.txt | ...
如果有足够的重复项和足够的 RAM 可以将结果保存在内存中,则可以通过输入文件一次性完成删除重复项,然后遍历所有剩余的值对:
#!/usr/bin/perl
use warnings;
use strict;
use feature qw/say/;
my %keys;
while (<>) {
chomp;
my ($col1, $col2, $col3) = split ' ';
$keys{$col1}{$col2} = $col3 unless exists $keys{$col1}{$col2};
}
$, = " ";
while (my ($col1, $sub) = each %keys) {
while (my ($col2, $col3) = each %$sub) {
next unless defined $col3;
if ($col1 lt $col2 && exists $keys{$col2}{$col1}) {
$col3 .= " $keys{$col2}{$col1}";
$keys{$col2}{$col1} = undef;
} elsif ($col2 lt $col1 && exists $keys{$col2}{$col1}) {
next;
}
say $col1, $col2, $col3;
}
}
为了提高效率,这会以任意未排序的顺序产生输出。
还有一种使用 sqlite 的方法(还需要大量额外的可用磁盘空间,并且列由制表符分隔,而不是任意空格):
#!/bin/sh
input="$1"
sqlite3 -batch -noheader -list temp.db 2>/dev/null <<EOF
.separator \t
PRAGMA page_size = 8096; -- Make sure the database can grow big enough
CREATE TABLE data(col1, col2, col3, PRIMARY KEY(col1, col2)) WITHOUT ROWID;
.import "$input" data
SELECT col1, col2, group_concat(col3, ' ')
FROM (
SELECT col1, col2, col3 FROM data WHERE col1 < col2
UNION ALL
SELECT col2, col1, col3 FROM data WHERE col2 < col1
)
GROUP BY col1, col2
ORDER BY col1, col2;
EOF
rm -f temp.db