从许多csv文件中删除重复项

时间:2012-10-15 03:04:05

标签: linux bash sorting csv uniq

鉴于n csv文件的大小合计为100 GB,我需要根据以下规则和条件删除重复的行:

  • csv文件编号为1.csv到n.csv,每个文件的大小约为50MB。
  • 第一列是字符串键,如果它们的第一列相同,则认为2行为dup。
  • 我希望通过将副本保留在以后的文件中来删除重复(2.csv被认为晚于1.csv)

我的算法如下,我想知道是否有更好的算法。

  • 将所有文件合并为一个巨型文件

    cat *.csv > one.csv
    
  • 对csv进行排序

    sort one.csv >one_sorted.csv
    
  • 此时不确定如何删除重复项。 uniq有一个跳过前N个字段的-f标志,但在我的情况下,我想跳过除前1个字段之外的所有字段。

我需要最后一步的帮助(消除已排序文件中的重复项)。还有更有效的算法吗?

4 个答案:

答案 0 :(得分:2)

这是使用GNU awk的一种方式:

awk -F, '{ array[$1]=$0 } END { for (i in array) print array[i] }' $(ls -v *.csv)

说明:读取数字排序的文件全局,我们将每个文件的第一列添加到一个关联数组,其值为整行。这样,保留的副本就是最新文件中出现的副本。完成后,循环遍历数组的键并打印出值。 GNU awk确实通过asort()asorti()函数提供排序功能,但将输出汇总到sort会使事情更容易阅读,并且可能更快更有效。

如果您需要对第一列进行数字排序,则可以执行此操作:

awk -F, '{ array[$1]=$0 } END { for (i in array) print array[i] | "sort -nk 1" }' $(ls -v *.csv)

答案 1 :(得分:1)

如果可以将行保留在内存中

如果有足够的数据适合内存,awk solutionsteve非常简洁,无论您是通过sort内的管道写入awk命令还是仅仅通过在shell级别将未经修饰的awk的输出传递给sort

如果你有100 GiB的数据,可能有3%的重复,那么你需要能够在内存中存储100 GiB的数据。这是很多主要的记忆。 64位系统可能会使用虚拟内存处理它,但它可能运行得相当慢。

如果键适合内存

如果您无法在内存中安装足够的数据,那么前面的任务就会变得更加困难,并且需要对文件进行至少两次扫描。我们需要假设,你至少可以在内存中放入每个密钥,并计算密钥出现的次数。

  1. 扫描1:读取文件。
    • 计算每个键在输入中出现的次数。
    • awk中,使用icount[$1]++
  2. 扫描2:重新读取文件。
    • 计算每个密钥出现的次数; ocount[$1]++
    • 如果icount[$1] == ocount[$1],则打印该行。
  3. (这假设您可以存储密钥并计数两次;另一种方法是在两次扫描中使用icount(仅限),在扫描1中递增,在扫描2中递减,在计数递减到的时间打印值零。)

    我可能会使用Perl而不是awk,只是因为在Perl中重新读取文件比在awk中更容易。


    甚至钥匙都不合适?

    如果你甚至无法将钥匙及其数量记入记忆中呢?然后你面临一些严重的问题,尤其是因为脚本语言可能不会像你想的那样干净地向你报告内存不足的情况。我不会试图越过这座桥,直到它被证明是必要的。如果有必要,我们需要一些关于文件集的统计数据来了解可能的内容:

    • 记录的平均长度。
    • 不同键的数量。
    • 每个N = 1,2,... max 的N次出现的不同键数。
    • 钥匙的长度。
    • 键数加上可以装入内存的计数。

    可能还有其他一些......所以,正如我所说的那样,让我们​​不要试图越过那座桥,直到它被证明是必要的。


    Perl解决方案

    示例数据

    $ cat x000.csv
    abc,123,def
    abd,124,deg
    abe,125,deh
    $ cat x001.csv
    abc,223,xef
    bbd,224,xeg
    bbe,225,xeh
    $ cat x002.csv
    cbc,323,zef
    cbd,324,zeg
    bbe,325,zeh
    $ perl fixdupcsv.pl x???.csv
    abd,124,deg
    abe,125,deh
    abc,223,xef
    bbd,224,xeg
    cbc,323,zef
    cbd,324,zeg
    bbe,325,zeh
    $ 
    

    请注意,缺少千兆字节测试!

    fixdupcsv.pl

    这使用'倒数,倒计时'技术。

    #!/usr/bin/env perl
    #
    # Eliminate duplicate records from 100 GiB of CSV files based on key in column 1.
    
    use strict;
    use warnings;
    
    # Scan 1 - count occurrences of each key
    
    my %count;
    my @ARGS = @ARGV;   # Preserve arguments for Scan 2
    
    while (<>)
    {
        $_ =~ /^([^,]+)/;
        $count{$1}++;
    }
    
    # Scan 2 - reread the files; count down occurrences of each key.
    # Print when it reaches 0.
    
    @ARGV = @ARGS;      # Reset arguments for Scan 2
    
    while (<>)
    {
        $_ =~ /^([^,]+)/;
        $count{$1}--;
        print if $count{$1} == 0;
    }
    

    while (<>)”符号会在执行任何操作之前销毁@ARGV(因此复制到@ARGS),但这也意味着如果您将@ARGV重置为原始版本值,它将第二次运行文件。在Mac OS X 10.7.5上使用Perl 5.16.0和5.10.0进行测试。

    这是Perl; TMTOWTDI。你可以使用:

    #!/usr/bin/env perl
    #
    # Eliminate duplicate records from 100 GiB of CSV files based on key in column 1.
    
    use strict;
    use warnings;
    
    my %count;
    
    sub counter
    {
        my($inc) = @_;
        while (<>)
        {
            $_ =~ /^([^,]+)/;
            $count{$1} += $inc;
            print if $count{$1} == 0;
        }
    }
    
    my @ARGS = @ARGV;   # Preserve arguments for Scan 2
    counter(+1);
    @ARGV = @ARGS;      # Reset arguments for Scan 2
    counter(-1);
    

    也有可能用来压缩循环体的方法,但我发现什么是合理清晰的,并且更倾向于清晰而非极端简洁。

    调用

    您需要以正确的顺序显示文件名的fixdupcsv.pl脚本。由于您的文件编号从1.csv到大约2000.csv,因此不要以字母数字顺序列出它们。其他答案建议ls -v *.csv使用GNU ls扩展选项。如果有,那是最好的选择。

    perl fixdupcsv.pl $(ls -v *.csv)
    

    如果没有,那么您需要对名称进行数字排序:

    perl fixdupcsv.pl $(ls *.csv | sort -t. -k1.1n)
    

    Awk解决方案

    awk -F, '
    BEGIN   {
                for (i = 1; i < ARGC; i++)
                {
                    while ((getline < ARGV[i]) > 0)
                        count[$1]++;
                    close(ARGV[i]);
                }
                for (i = 1; i < ARGC; i++)
                {
                    while ((getline < ARGV[i]) > 0)
                    {
                        count[$1]--;
                        if (count[$1] == 0) print;
                    }
                    close(ARGV[i]);
                }
            }' 
    

    这会忽略awk先天的“读取”循环并明确地进行所有读取(您可以用END替换BEGIN并获得相同的结果)。逻辑在很多方面都基于Perl逻辑。在Mac OS X 10.7.5上使用BSD awk和GNU awk进行测试。有趣的是,GNU awk坚持调用close的括号,其中BSD awk没有。第一个循环中必须进行close()调用才能使第二个循环完成。第二个循环中的close()调用是为了保持对称性和整洁性 - 但是当你在一次运行中处理几百个文件时它们也可能是相关的。

答案 2 :(得分:0)

我的回答基于steve

awk -F, '!count[$1]++' $(ls -rv *.csv)
在awk语句中隐含了

{print $0}

基本上awk仅打印$ 1包含该值的第一行。由于.csv文件以颠倒的自然顺序列出,这意味着对于$ ​​1具有相同值的所有行,仅打印最新文件中的一行。

注意:如果您在同一个文件中有重复项(例如,如果您在同一个文件中有多个相同键的实例),这将工作

答案 3 :(得分:0)

关于您的排序计划,对单个文件进行排序然后合并它们可能更实际,而不是连接然后排序。使用sort程序排序的复杂性可能是O(n log(n))。如果您说每个50MB文件有200000行,而2000个文件,则n将大约为4亿,n log(n) ~ 10^10。相反,如果您分别处理R记录的F文件,则排序成本为O(F*R*log(R)),合并成本为O(F*R*log(R))。这些成本足够高,以至于单独的排序不一定更快,但是可以将过程分解为方便的块,以便随着事情的进展更容易检查。这是一个小规模示例,假设逗号可以用作排序键的分隔符。 (包含逗号的引号分隔的键字段对于排序是一个问题,如图所示。)请注意-s告诉sort执行稳定排序,在顺序中保留具有相同排序键的行他们遇到了。

for i in $(seq 1 8); do sort -t, -sk1,1 $i.csv > $i.tmp; done
sort -mt, -sk1,1 [1-8].tmp > 1-8.tmp

或者如果更谨慎可能会节省一些中间结果:

sort -mt, -sk1,1 [1-4].tmp > 1-4.tmp
sort -mt, -sk1,1 [5-8].tmp > 5-8.tmp
cp 1-4.tmp 5-8.tmp /backup/storage
sort -mt, -sk1,1 1-4.tmp 5-8.tmp > 1-8.tmp

此外,在合并或合并之后执行单独排序的优点是可以轻松地跨多个处理器或系统拆分工作负载。

在对所有文件进行排序和合并(比如文件X)后,编写一个awk程序非常简单,该程序在BEGIN从X读取一行并将其放入变量L.此后,每次读取一个从X开始,如果$ 0的第一个字段与L不匹配,则写出L并将L设置为$ 0。但如果$ 0与L匹配,则将L设置为$ 0。在END,它写出L。