将非常大的csv文件与公共列合并

时间:2012-08-20 17:07:10

标签: perl unix join csv awk

例如,我有两个csv文件, 0.csv

100a,a,b,c,c
200a,b,c,c,c
300a,c,d,c,c

和 1.csv

100a,Emma,Thomas
200a,Alex,Jason
400a,Sanjay,Gupta
500a,Nisha,Singh

我希望输出像

100a,a,b,c,c,Emma,Thomas
200a,b,c,c,c,Alex,Jason
300a,c,d,c,c,0,0
400a,0,0,0,0,Sanjay,Gupta
500a,0,0,0,0,Nisha,Singh

我如何在Unix shell脚本或Perl中执行此操作?我知道unix“join”命令,这对小文件很有用。例如,为了获得我的结果,我可以做到

join -t , -a 1 -a 2 -1 1 -2 1 -o 0 1.2 1.3 1.4 1.5 2.2 2.3 -e "0" 0.csv 1.csv

但这对我的目的来说是不可行的,因为我的实际数据文件有超过一百万列(总数据大小为千兆字节),因此我的unix命令也将超过一百万个字符。这可能是最重要的问题,因为低效的代码会很快陷入困境。

另请注意,每当缺少数据时,我都需要占位符字符“0”。这使我无法简单地使用此

join -t , -a 1 -a 2 -1 1 -2 1 0.csv 1.csv

也是初学Perl程序员,所以一些细节真的很受欢迎。我更喜欢解决方案是perl或shell脚本,但实际上任何有效的方法都可以。

6 个答案:

答案 0 :(得分:1)

您也可以使用awk执行此操作。

确定两个文件中最宽行的长度,并将其保存到max0max1

awk -F, '
  ARGIND == 1 && NF > max0 { max0 = NF }
  ARGIND == 2 && NF > max1 { max1 = NF }
  END { print max0, max1 }
' 0.csv 1.csv | read max0 max1

使用此awk脚本进行连接:

<强> foo.awk

BEGIN { 
  max1--
  FS  = OFS = ","
}

ARGIND == 1 {
  A[$1] = $2

  # Copy columns from first file to key
  for(i=3; i<=NF; i++)
    A[$1] = A[$1] FS $i

  # Pad until we have max0 columns
  for( ; i<=max0; i++)
    A[$1] = A[$1] FS "0"
}

ARGIND == 2 {
  # Pad rows which are only in second file
  if(A[$1] == "") {
    A[$1] = 0
    for(i=3; i<=max0; i++)
      A[$1] = A[$1] FS "0"
  }

  # Copy columns from second file to key
  for(i=2; i<=NF; i++)
    A[$1] = A[$1] FS $i

  # Pad until we have max1 columns
  for( ; i<=max1; i++)
    A[$1] = A[$1] FS "0"
}

END { 
  for(key in A) {
    # Pad rows which are only in first file
    split(A[key], fields, ",")
    for(i=1; i <= max0+max1-length(fields)-1; i++)
      A[key] = A[key] FS "0"

    # Finally print key and accumulated column values
    print key, A[key]
  }
}

使用以下命令运行:

awk -f foo.awk -v max0=$max0 -v max1=$max1 0.csv 1.csv | sort -n

使用-v传入最宽的行值。输出来自哈希并且未排序,因此在显示之前sort -n

答案 1 :(得分:1)

如果您可以为每个文件添加标题,则可以使用tabulator来解决问题。例如:

0.csv:

key,letter_1,letter_2,letter_3,letter_4
100a,a,b,c,c
200a,b,c,c,c
300a,c,d,c,c

1.csv:

key,name_1,name_2
100a,Emma,Thomas
200a,Alex,Jason
400a,Sanjay,Gupta
500a,Nisha,Singh

然后tbljoin -lr -n 0 0.csv 1.csv生成

key,letter_1,letter_2,letter_3,letter_4,name_1,name_2
100a,a,b,c,c,Emma,Thomas
200a,b,c,c,c,Alex,Jason
300a,c,d,c,c,0,0
400a,0,0,0,0,Sanjay,Gupta
500a,0,0,0,0,Nisha,Singh

请注意(与纯unix join命令相反),输入文件不需要排序;此外,您不必担心内存消耗,因为实现基于unix排序,并将采用基于文件的大型文件合并排序。

答案 2 :(得分:0)

当您处理大量数据并且两个源的大小大致相同时,合并连接是最佳选择。这是因为一旦两个(每个)源被排序,它就会使用恒定的内存量。合并连接也是完全外连接的一个很好的选择,它可以在Perl中非常优雅地编写。

对于使用以下Perl脚本,您必须在第一列中按键按字典顺序排序这两个文件,并且键必须是唯一的。它还假设两个文件中每行的列数完全相同。

#!/usr/bin/perl

use strict;
use warnings;
use Text::CSV_XS;

die "Usage $0 file1.csv file2.csv" unless @ARGV > 1;

my ( $file1, $file2 ) = @ARGV;

open my $fh1, '<', $file1 or die "Can't open $file1: $!";
open my $fh2, '<', $file2 or die "Can't open $file2: $!";

my $csv = Text::CSV_XS->new( { binary => 1, eol => "\n" } );

my $r1 = $csv->getline($fh1) or die "Missing data in $file1";
my $r2 = $csv->getline($fh2) or die "Missing data in $file2";

# same amount of zeros as number of fields in each file
my @cols1 = (0) x ( @$r1 - 1 );
my @cols2 = (0) x ( @$r2 - 1 );

while ( $r1 || $r2 ) {    # there are some data

    # compare keys only if there are rows in both files
    # zero silences warnings in numeric comparisons below
    my $cmp = $r1 && $r2 && ( $$r1[0] cmp $$r2[0] ) || 0;

    # row is defined and has less or equal key than another one
    my $le1 = $r1 && $cmp < 1;
    my $le2 = $r2 && $cmp > -1;

    $csv->print(
        *STDOUT,
        [   $le1 ? $$r1[0] : $$r2[0],    # key
            ( $le1 ? @$r1[ 1 .. @cols1 ] : @cols1 ),    # first file fields
            ( $le2 ? @$r2[ 1 .. @cols2 ] : @cols2 )     # second file fields
        ]
    );

    #read next rows
    $r1 = $csv->getline($fh1) if $le1;
    $r2 = $csv->getline($fh2) if $le2;
}

用法为script.pl 0.csv 1.csv > result.csv。如果没有排序,请使用sort -u -d -t, -k1,1对文件进行排序。

脚本以线性时间工作(当已经排序时)并且仅使用存储器来存储来自每个文件的一行,即&#34;常数&#34;大小

您可以使用

对脚本中的文件进行排序
$ENV{LC_ALL} = 'C';
open my $fh1, "( sed '1!d' $file1; sed 1d $file1 | sort -u -d -t, -k1,1 ) |"
    or die "Can't sort $file1: $!";
open my $fh2, "( sed '1!d' $file2; sed 1d $file2 | sort -u -d -t, -k1,1 ) |"
    or die "Can't sort $file2: $!";

答案 3 :(得分:-1)

如果您的文件足够小以适应RAM,这应该很容易。您可以使用Perl读取文件,解析它们,并使用公共行中的值作为哈希键将行推送到数组哈希中。 然后通过其键遍历散列的内容并打印出数组。

答案 4 :(得分:-1)

这就是我提出的(perl):

my $output={};

open FILE1, '</path/to/file1';
while (<FILE1>){
    chomp;
    my @values=split(/,/, $_);
    my $id=shift(@values);
    if($output->{$id}){
        my $temparray=$output->{$id};
        push (@$temparray, @values);
    }else{
        $output->{$id}=@values;
    }
}
close FILE1;
open FILE2, '</path/to/file2';
while (<FILE2>){
    chomp;
    my @values=split(/,/, $_);
    my $id=shift(@values);
    if($output->{$id}){
        my $temparray=$output->{$id};
        push (@$temparray, @values);
    }else{
        $output->{$id}=@values;
    }   
}
close FILE2;

答案 5 :(得分:-1)

csvkit是一个处理csv文件并允许此类连接(以及其他功能)的工具。

csvjoin。它的命令行界面很紧凑,它处理大量的csv格式(tsv,其他分隔符,编码,转义字符等)。

您要求的内容可以使用:

csvjoin --columns 0 0.csv 1.csv