`uniq`没有排序一个巨大的文本文件?

时间:2015-06-18 03:55:43

标签: bash awk

我有一个愚蠢的大文本文件(即今天的40千兆字节),我想过滤唯一的行而不排序文件。

该文件具有unix行结尾,所有内容都匹配[[:print:]]。我尝试了以下awk脚本来只显示唯一的行:

awk 'a[$0] {next} 1' stupid.txt > less_stupid.txt

我的想法是,我通过引用其元素来填充数组,使用文件的内容作为键,然后跳过已经在数组中的行。但这有两个原因失败 - 首先,因为它莫名其妙地无法工作(即使是在小型测试文件上),其次是因为我知道在加载整组唯一行之前我的系统会耗尽内存通过awk进入记忆。

搜索后,我发现this answer建议:

awk '!x[$0]++'

虽然这适用于小文件,但在读取整个文件之前也会耗尽内存。

什么是更好(即工作)的解决方案?我对几乎所有事情都持开放态度,尽管我更倾向于使用我所知道的语言解决方案(bash& awk,因此也就是标签)。在尝试可视化问题时,我提出的最好的方法是存储一系列行校验和或MD5而不是行本身,但这只会节省一点空间并冒着校验和冲突的风险。 / p>

任何提示都会非常受欢迎。告诉我这是不可能的也是受欢迎的,所以我不想试图解决它。 :-P

6 个答案:

答案 0 :(得分:5)

awk '!x[$0]++'技巧是在没有排序的情况下对文件或流进行重复数据删除的最优雅解决方案之一。但是,它在内存方面效率低,不适合大文件,因为它将所有独特的行保存到内存中。

但是,更有效的实现方法是在数组中保存行的常量长度哈希表示,而不是整行。您可以使用Perl在一行中实现此目的,它与awk脚本非常相似。

perl -ne 'use Digest::MD5 qw(md5_base64); print unless $seen{md5_base64($_)}++' huge.txt

这里我使用md5_base64而不是md5_hex,因为base64编码需要22个字节,而十六进制表示32个。

但是,由于hashes的Perl实现仍然需要大约120字节的每个密钥,因此您的内存可能很快就会占用大量文件。

在这种情况下,解决方案是以块的形式处理文件,手动拆分或使用带有--pipe, - keep-order和--block选项的GNU Parallel(利用重复的事实)如你所提到的那样,线条相距不远。以下是parallel

的方法
cat huge.txt | pv | 
parallel --pipe --keep-order --block 100M -j4 -q \
perl -ne 'use Digest::MD5 qw(md5_base64); print unless $seen{md5_base64($_)}++' > uniq.txt

--block 100M选项告诉parallel并行处理100MB的块输入。 -j4表示并行启动4个进程。这里一个重要的参数是--keep-order,因为您希望唯一的行输出保持相同的顺序。我已经在管道中包含了pv,以便在长时间运行的进程执行时获得一些不错的统计信息。

在我使用随机数据1GB文件执行的基准测试中,我使用上述设置达到130MB /秒的吞吐量,这意味着您可以在4分钟内删除40GB文件(如果您有足够快的硬盘能够写这个速度)。

其他选项包括:

  • 使用高效的trie结构来存储密钥并检查重复项。例如,一个非常有效的实现是用marisa-trie在C ++中编码的wrappers in Python
  • 使用external merge sort或分发/ bucket排序
  • 对您的大文件进行排序
  • 将您的文件存储在数据库中,并在包含您的行或最有效md5_sums行的索引列上使用SELECT DISTINCT。
  • 或使用bloom filters

以下是使用Perl的Bloom::Faster模块的示例:

perl -e 'use Bloom::Faster; my $f = new Bloom::Faster({n => 100000000, e => 0.00001}); while(<>) { print unless $f->add($_); }' huge.txt > uniq.txt

您可以从Bloom::Fastercran然后sudo cran)安装install "Bloom::Faster"

说明:

  • 您必须指定概率错误率e和可用存储桶数n。每个桶所需的内存大约为2.5个字节。如果你的文件有1亿个独特的行,那么你将需要1亿个桶和大约260MB的内存。
  • 如果密钥(即此处的行)是重复的,$f->add($_)函数会将一行的哈希值添加到过滤器并返回true
  • 您可以估算文件中唯一行的数量,使用dd if=huge.txt bs=400M count=1 | awk '!a[$0]++' | wc -l(400MB)解析文件的一小部分,然后将该数字乘以100(40GB)。然后将n选项设置得更高一些,以确保安全。

在我的基准测试中,这种方法实现了6MB / s的处理速度。您可以将此方法与上面的GNU parallel建议结合使用,以利用多个内核并实现更高的吞吐量。

答案 1 :(得分:3)

我没有方便的数据(或类似的东西),所以我无法对此进行测试,但这里有一个概念验证:

$ t='one\ntwo\nthree\none\nfour\nfive\n'
$ printf "$t" | nl -w14 -nrz -s, | sort -t, -k2 -u | sort -n | cut -d, -f2-
one
two
three
four
five

我们的原始数据包括一条重复的行。管道功能如下:

  • nl添加行号。它是一种标准的,低影响力的unix工具。
  • sort第一次对SECOND字段进行'排序 - 在nl之前的那一行开始。根据您的数据需要进行调整。
  • sort第二次按照nl命令定义的顺序重新开始。
  • cut只是删除了行号。有多种方法可以做到这一点,但其中一些方法取决于您的操作系统。这个是便携式的,适用于我的例子。

现在......对于淫秽的大文件,sort命令需要一些额外的选项。特别是--buffer-size--temporary-directory。请阅读man sort了解详情。

我不能说我希望这是 fast ,我怀疑你会使用大量的磁盘IO,但我不明白为什么它至少不会工作

答案 2 :(得分:3)

假设您可以首先对文件进行排序(即您可以让sort file工作),那么我认为这样的事情可能会起作用(取决于是否有大型awk脚本)在内存使用/等方面,文件比大型awk数组要好。)。

sort file | uniq -dc | awk '{gsub("\"", "\\\"", $0); print "$0==\""substr($0, index($0, $1) + 2)"\"{x["NR"]++; if (x["NR"]>1){next}}"} END{print 7}' > dedupe.awk
awk -f dedupe.awk file

在测试输入文件中,如:

line 1
line 2
line 3
line 2
line 2
line 3
line 4
line 5
line 6

创建一个awk脚本:

$0=="line 2"{x[1]++; if (x[1]>1){next}}
$0=="line 3"{x[2]++; if (x[2]>1){next}}
7

并以awk -f dedupe.awk file输出方式运行:

line 1
line 2
line 3
line 4
line 5
line 6

如果awk脚本本身的大小是一个问题(可能不太可能),你可以通过使用另一个sentinel值来减少它:

sort file | uniq -dc | awk 'BEGIN{print "{f=1}"} {gsub("\"", "\\\"", $0); print "$0==\""substr($0, index($0, $1) + 2)"\"{x["NR"]++;f=(x["NR"]<=1)}"} END{print "f"}'

每行减少7个字符(如果你从原版中删除空格则为6个)并生成:

{f=1}
$0=="line 2"{x[1]++;f=(x[1]<=1)}
$0=="line 3"{x[2]++;f=(x[2]<=1)}
f

此解决方案可能会运行得更慢,因为它不会在找到匹配项时使脚本短路。

如果awk脚本的运行时太大,甚至可以通过根据匹配计数对重复行进行排序来改善时间(但这是否与数据有关,是否相关)。

答案 3 :(得分:3)

我会这样做:

#! /bin/sh
usage ()
{
    echo "Usage:  ${0##*/} <file> [<lines>]" >&2
    exit 1
}


if [ $# -lt 1 -o $# -gt 2 -o ! -f "$1" ]; then usage; fi
if [ "$2" ]; then
    expr "$2" : '[1-9][0-9]*$' >/dev/null || usage
fi

LC_ALL=C
export LC_ALL

split -l ${2:-10000} -d -a 6 "$1"

for x in x*; do
    awk '!x[$0]++' "$x" >"y${x}" && rm -f "$x"
done

cat $(sort -n yx*) | sort | uniq -d | \
    while IFS= read -r line; do
        fgrep -x -n "$line" /dev/null yx* | sort -n | sed 1d | \
            while IFS=: read -r file nr rest; do
                sed -i -d ${nr}d "$file"
            done
    done

cat $(sort -n yx*) >uniq_"$1" && rm -f yx*

(概念证明;在用于生产之前需要更多抛光)。

这里发生了什么:

  • split将文件拆分为10000行(可配置);这些块名为x000000x000001,...
  • awk从每个块中删除重复项,而不会弄乱行顺序;生成的文件为yx000000yx000001,...(因为awk无法移植到位)
  • cat $(sort -n yx*) | sort | uniq -d重新组合块并找到重复列表;由于块的构造方式,每个重复的行在每个块中最多可出现一次
  • fgrep -x -n "$line" /dev/null yx*查找每个重复行所在的位置;结果是一行yx000005:23:some text
  • sort -n | sed 1d从上面的列表中删除第一个块(这是该行的第一次出现,应该保持不变)
  • IFS=: read -r file nr restyx000005:23:some text拆分为file=yx000005nr=23,其余
  • sed -i -e ${nr}d "$file"从块$nr
  • 中删除行$file
  • cat $(sort -n yx*)重新组装块;他们需要进行分类,以确保它们的顺序正确。

这可能不是很快,但我认为它应该有效。从10000增加每个块中的行数可以加快速度,但代价是使用更多内存。对于跨块的重复行数,操作为O(N^2);幸运的是,这不会太大。

以上假定GNU sed-i)。它还假设当前目录中没有名为x*yx*的文件(可能使用某些清理的部分,可能是将垃圾移动到由mktemp -d创建的目录中)。

编辑第二个版本,来自@EtanReisner的反馈:

#! /bin/sh
usage ()
{
    echo "Usage:  ${0##*/} <file> [<lines>]" >&2
    exit 1
}


if [ $# -lt 1 -o $# -gt 2 -o ! -f "$1" ]; then usage; fi
if [ "$2" ]; then
    expr "$2" : '[1-9][0-9]*$' >/dev/null || usage
fi

tdir=$(mktemp -d -p "${TEMP:-.}" "${0##*/}_$$_XXXXXXXX") || exit 1
dupes=$(mktemp -p "${TEMP:-.}" "${0##*/}_$$_XXXXXXXX") || exit 1

trap 'rm -rf "$tdir" "$dupes"' EXIT HUP INT QUIT TERM

LC_ALL=C
export LC_ALL

split -l ${2:-10000} -d -a 6 "$1" "${tdir}/x"

ls -1 "$tdir" | while IFS= read -r x; do
    awk '!x[$0]++' "${tdir}/${x}" >"${tdir}/y${x}" && \
    rm -f "${tdir}/$x" || exit 1
done

find "$tdir" -type f -name 'yx*' | \
    xargs -n 1 cat | \
    sort | \
    uniq -d >"$dupes" || exit 1

find "$tdir" -type f -name 'yx*' -exec fgrep -x -n -f "$dupes" /dev/null {} + | \
    sed 's!.*/!!' | \
    sort -t: -n -k 1.3,1 -k 2,2 | \
    perl '
        while(<STDIN>) {
            chomp;
            m/^(yx\d+):(\d+):(.*)$/o;
            if ($dupes{$3}++)
                { push @{$del{$1}}, int($2) }
            else
                { $del{$1} = [] }
        }
        undef %dupes;

        chdir $ARGV[0];

        for $fn (sort <"yx*">) {
            open $fh, "<", $fn
                or die qq(open $fn: $!);
            $line = $idx = 0;
            while(<$fh>) {
                $line++;
                if ($idx < @{$del{$fn}} and $line == $del{$fn}->[$idx])
                    { $idx++ }
                else
                    { print }
            }
            close $fh
                or die qq(close $fn: $!);
            unlink $fn
                or die qq(remove $fn: $!);
        }
    ' "$tdir" >uniq_"$1" || exit 1

答案 4 :(得分:1)

如果有很多重复,一种可能性是使用split(1)将文件拆分为可管理的部分,并使用像sort / uniq这样的常规内容来汇总唯一的行。这将比实际作品本身短。在此之后,您可以比较各个部分以得出实际摘要。

答案 5 :(得分:1)

也许不是你一直在寻找的答案,但这里是:使用布隆过滤器。 https://en.wikipedia.org/wiki/Bloom_filter这类问题是其存在的主要原因之一。