【问题标题】:awk/sed remove duplicates and merge permuted columnsawk/sed 删除重复并合并排列的列
【发布时间】:2019-05-04 06:40:04
【问题描述】:

我有以下文件:

ABC     MNH     1
UHR     LOI     2    
QWE     LOI     3
MNH     ABC     4
PUQ     LOI     5
MNH     ABC     6
QWE     LOI     7
LOI     UHR     8    

我想删除所有重复项(基于前两列 - 例如,第 6 行是第 4 行的重复项)。我还想合并第 1 列和第 2 列被置换的条目(例如,第 1 行和第 4 行)。这意味着此列表应导致:

ABC     MNH     1 4
UHR     LOI     2 8
QWE     LOI     3
PUQ     LOI     5

但是,这个文件很大。大约 2-3 TB。这可以用 awk/sed 完成吗?

【问题讨论】:

  • 到目前为止你有什么尝试?
  • 这是一个非常大的文件。通常,当人们说他们有一个巨大的文件时,这很有趣,结果是 20 MB,我们就像“没问题!”。无论您尝试什么,或建议什么,我肯定会先在文件的 20 MB 块上尝试它,然后将它花费的时间乘以 150000 以查看它是否实用。此外,在一个未排序的文件上一次性解决此问题的典型方法是在您继续加载内存时,在第一列和第二列中使用所有唯一的与顺序无关的对。这取决于重复的百分比是否可行。
  • 实际文件中的字符串有多长。他们总是3个字符吗?这只是为了知道可能组合的数量。如果它们是 3,那么您只有 26^6 种可能的唯一组合,因此可以使用 awk 进行管理。
  • 等一下。行尾的这些数字是否真的存在于您的数据中,或者您只是想向我们展示输入/输出中的输入行号?

标签: awk


【解决方案1】:

我不明白为什么您发布的是您的预期输出,因此您可能需要对其进行按摩,但恕我直言,这是解决问题的正确方法,因此只有“排序”处理在内部存储多 TB 输入(和 sort 旨在通过分页等来做到这一点),而 awk 脚本一次只处理一行并且在内存中保留的很少:

$ cat tst.sh
#!/bin/env bash

awk '{print ($1>$2 ? $1 OFS $2 : $2 OFS $1), $0}' "$1" |
sort -k1,2 |
awk '
    { curr = $1 OFS $2 }
    prev != curr {
        if ( NR>1 ) {
            print rec
        }
        rec = $0
        sub(/^([^[:space:]]+[[:space:]]+){2}/,"",rec)
        prev = curr
        next
    }
    { rec = rec OFS $NF }
    END { print rec }
'

$ ./tst.sh file
ABC     MNH     1 4 6
PUQ     LOI     5
QWE     LOI     3 7
LOI     UHR     8 2

在下面的 cmets 中与@kvantour 讨论后的替代实现(-s 稳定排序需要 GNU 排序):

$ cat tst.sh
#!/bin/env bash

awk '{print ($1>$2 ? $1 OFS $2 : $2 OFS $1), $0}' "$1" |
sort -s -k1,2 |
awk '
    { curr = $1 OFS $2 }
    prev != curr {
        if ( NR>1 ) {
            print rec
        }
        rec = $0
        sub(/^([^[:space:]]+[[:space:]]+){2}/,"",rec)
        sub(/[[:space:]]+[^[:space:]]+$/,"",rec)
        delete seen
        prev = curr
    }
    !seen[$3,$4]++ { rec = rec OFS $NF }
    END { print rec }
'

$ ./tst.sh file
ABC     MNH 1 4
PUQ     LOI 5
QWE     LOI 3
UHR     LOI 2 8

【讨论】:

  • 我不确定,但我不认为第一个管道和排序将能够处理 2TB 文件。
  • 此外,在您的示例输出中,第一行中的 6 不应出现,因为之前已经看到了组合键 MNH ABC,其值为 4。这也意味着 sort 命令可能会改变重复键的原始顺序,这将影响输出。
  • 关于管道和排序,这里有一些有趣的信息:stackoverflow.com/questions/43362433/…
  • 我相信像sort -s -T /path/to/extra/harddisk -S4G 这样的东西可能会做到这一点。 @riasc 如果上述方法不起作用,请告诉我们,我们将尝试提出另一种解决方案。
【解决方案2】:

总是乐于助人的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

【讨论】:

    【解决方案3】:

    如果您的前两列最多只有 3 个字符,那么前两列将有 26^6 种可能的组合。这很容易用 awk 处理。

    { key1=$1$2; key2=$2$1 }
    (key1 in a) { next }                   # duplicate :> skip
    (key2 in a) { print $2,$1,a[key2],$3 } # permutation :> print
    { a[key1]=$3 }                         # store value
    

    然而,这只会打印排列,并根据要求打印最多 2 个元素。因此,如果找到排列,数组a 将同时包含key1 和排列后的键key2,否则它将只有key1

    如果已经打印了排列,则可以使用第二个数组来跟踪它。叫它b。这样您就可以从 a 中删除 2 个元素,同时跟踪 b 中的一个元素:

    { key1=$1$2; key2=$2$1 }
    (key1 in b) || (key2 in b) { next }  # permutation printed, is duplicate
    (key1 in a)                { next }  # only duplicate, no permutation found
    (key2 in a) {                        # permutation found 
                  print $2,$1,a[key2],$3 # - print
                  delete a[key1]         # - delete keys from a
                  delete a[key2]
                  b[key1]                # - store key in b
                  next                   # - skip the rest
                }
     { a[key1]=$3 }
     END { for (k in a) { print substr(1,3,k),substr(4,3,k),a[k] } }
    

    【讨论】:

    • @EdMorton 我假设总是 3 个字符。我想保存 subsep 的字节
    • @EdMorton 数组a 中的每个条目代表找到的原始key1。我测试key1 是否在数组中以检查重复项,但如果key2 在数组中,我们遇到了排列。最后,您应该在数组中同时拥有key1key2,您可以将它们用于进一步的复制。有一种方法可以清理数组。
    猜你喜欢
    • 2015-01-01
    • 1970-01-01
    • 2016-10-24
    • 1970-01-01
    • 2020-08-16
    • 1970-01-01
    • 2013-05-02
    • 2011-11-18
    • 2016-04-27
    相关资源
    最近更新 更多