【问题标题】:remove dups from many csv files从许多 csv 文件中删除 dups
【发布时间】:2012-10-05 00:30:23
【问题描述】:

鉴于 n 个 csv 文件的大小加起来达到 100 GB,我需要根据以下规则和条件删除重复的行:

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

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

  • 将所有文件合并为一个大文件

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

    sort one.csv >one_sorted.csv
    
  • 目前不确定如何消除重复。 uniq 有一个 -f 标志可以跳过前 N 个字段,但就我而言,我想跳过除前 1 个字段之外的所有字段。

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

【问题讨论】:

  • 除非你用文件号标记行,否则排序将意味着你不能再满足第三个(后来的条目获胜)条件。至少,sort -o one_sorted.csv *.csv 将比cat + sort 选项占用更少的磁盘空间并且更快。
  • 您希望找到多少重复项?你认为去重输出会有多大?
  • @Jonathan Leffler:如果我们不能保持第三个条件,那么让我们忘记它。很少有重复(可能少于 3%)
  • 每条记录有多大(平均)?每个键有多大(平均)?大小的可变性如何?这个工作负载在多大的机器上运行(机器上的主内存是多少)?

标签: linux bash sorting csv uniq


【解决方案1】:

这是使用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:如果awk 可以为内存中的每个唯一键保存一条记录,那就太好了。
  • 对于大约 100GB 的文本数据我有疑问:}
  • @steve:如果内存不足,程序会中断吗?
  • @user121196:如果它消耗的内存比可用内存多,你的整个系统就会崩溃。使用大型机器让生活更轻松。
【解决方案2】:

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

如果足够多的数据可以放入内存,那么steveawk solution 非常简洁,无论您是通过awk 中的管道写入sort 命令,还是简单地通过管道输出未经修饰的@ 987654328@ 到 sort 在 shell 级别。

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

如果密钥适合内存

如果您无法在内存中容纳足够多的数据,那么接下来的任务就会更加困难,并且至少需要对文件进行两次扫描。我们需要假设,您至少可以将每个键放入内存中,并计算该键出现的次数。

  1. 扫描 1:读取文件。
    • 计算每个键在输入中出现的次数。
    • awk 中,使用icount[$1]++
  2. 扫描 2:重新读取文件。
    • 计算每个键出现的次数; ocount[$1]++
    • 如果是icount[$1] == ocount[$1],则打印该行。

(假设您可以存储键和计数两次;另一种方法是在两次扫描中使用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 (&lt;&gt;)”符号会破坏@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 的固有“读取”循环并显式执行所有读取(您可以将 BEGIN 替换为 END 并获得相同的结果)。该逻辑在许多方面都基于 Perl 逻辑。在 Mac OS X 10.7.5 上使用 BSD awk 和 GNU awk 进行测试。有趣的是,GNU awk 在对 close 的调用中坚持使用括号,而 BSD awk 没有。 close() 调用在第一个循环中是必要的,以使第二个循环完全工作。第二个循环中的 close() 调用是为了保持对称性和整洁性 - 但当您在一次运行中处理几百个文件时,它们也可能是相关的。

【讨论】:

  • +1 是的,我同意如果 OP 无法访问足够大的机器,那么他将不得不“Perl”它。使用awk 并不容易。我喜欢第二种(替代方法),因为它是最有效的方法,尤其是在键非常大的情况下。
  • @Jonathan Leffler:密钥将适合内存。你会在awk中提供真实的代码吗?
  • 如果我可以多次投票,我会的。我喜欢perl 解决方案,但它可能更性感。例如,您可以将第一个循环替换为$count{ (split(',', $_))[0] }++ while &lt;&gt;;。它也可能更易于维护。不要忘记包括如何运行野兽:./script.pl $(ls -v *.csv)
  • @user121196:perl 解决方案应该是公认的答案。
  • @steve:这是 Perl — TMTOWTDI!一个有趣的变体两次使用相同的函数,参数 +1 和 -1:my @ARGS = @ARGV; counter(+1); @ARGV = @ARGs; counter(-1); 和子 counter 进行读取和打印。由于增量,它不会在第一次调用时打印任何内容;由于递减,它将在第二次调用时打印。我不确定拆分整个字符串然后丢弃除第一个拆分字段之外的所有内容是否有效。
【解决方案3】:

我的回答是基于steve

awk -F, '!count[$1]++' $(ls -rv *.csv)

{print $0} 隐含在 awk 语句中。

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

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

【讨论】:

  • 问题要求使用相同键的最后一行,而不是使用给定键的第一行。这让生活变得复杂!如果您在同一个文件中有重复项,为什么该机制不起作用?看来我应该没问题。
  • @JonathanLeffler:是的,我最初考虑过这种确切的方法,但不幸的是,如果它们是同一文件中的重复项,则数组不会保存最新的键。它将保存密钥的第一个实例。
  • @JonathanLeffler:OP 明确提到了跨不同文件的 dup (相同的键)(在这种情况下,代码将起作用)。但是他没有在同一个文件中提到 dups(在这种情况下,代码将不起作用 - 这可能是您在这里主要关心的问题,但请注意,我已经在 Note 中提到了这个限制在我的回答中)
  • 好的——我现在明白了我遗漏了什么,以及你仔细没有突出显示的内容。这是 ls 选项中的字母 r。这会以相反的顺序列出文件。它还解释了为什么如果单个文件中有重复条目,您的算法将不起作用。或许,您应该强调这些要点。
  • @JonathanLeffler:我有点做过,引用“......因为 .csv 文件以 reversed 自然顺序列出”
【解决方案4】:

关于您的排序计划,对单个文件进行排序然后合并它们可能更实际,而不是连接然后排序。使用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。

【讨论】:

  • 任何从数据排序开始的算法的困难在于,您会丢失关于给定键的哪一行最后出现的位置信息。
  • @JonathanLeffler,你错了。我指定 -s 以获得稳定的排序。但是,我也应该指定-k1,1(可能还有-t,)并相应地编辑了我的答案。
  • 我在扫描您的答案时错过了-s - 抱歉。很明显,我该睡觉了。
猜你喜欢
  • 1970-01-01
  • 2011-12-15
  • 1970-01-01
  • 2011-05-03
  • 1970-01-01
  • 2023-01-12
  • 1970-01-01
  • 2019-11-17
  • 2016-11-05
相关资源
最近更新 更多