例如,我有两个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命令也将超过一百万个字符长。 这可能是最重要的头痛,因为低效的代码很快就陷入了困境。
另外请注意,每当缺less数据时,我需要占位符字符“0”。 这可以防止我简单地使用这个
join -t , -a 1 -a 2 -1 1 -2 1 0.csv 1.csv
也是一个初学Perl程序员,所以一些细节真的很受欢迎。 我更喜欢解决scheme是Perl或shell脚本,但真的什么工作将罚款。
你也可以用awk
来做到这一点。
确定两个文件中最宽行的长度,并将其保存为max0
和max0
:
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
。
如果你可以为每个文件添加一个头文件,那么你可以使用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排序的,并且会对大文件采取基于文件的合并排序。
当你处理大量的数据并且两个数据源具有大致相同的大小时,合并连接是最好的选择。 这是因为一旦两个(每个)源都被排序,它就会使用恒定的内存量。 合并连接也是完全外连接的一个很好的选择,它可以在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
请使用sort -u -d -t, -k1,1
排序文件。
该脚本以线性时间工作(当已经排序时)并且仅使用存储器来存储来自每个文件的一行,即“常量”大小。
您可以使用脚本对文件进行排序
$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: $!";
如果你的文件足够小以适应内存,这应该很容易。 您可以使用Perl来读取文件,解析它们,并使用公共行中的值作为散列键将行推入数组的散列。 然后通过键遍历散列的内容并打印出数组。
这是我想出来的(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;
csvkit
是一个处理CSV文件并允许这样的连接(以及其他功能)的工具。
见csvjoin
。 它的命令行界面非常紧凑,可处理多种csv格式(tsv,其他分隔符,编码,转义字符等)
你所要求的可以使用:
csvjoin --columns 0 0.csv 1.csv