使用 Perl 或 Powershell,如何比较 2 个 CSV 文件并只获取新行?
Using Perl or Powershell, how to compare 2 CSV files and get only the new rows?
我正在比较两个以逗号分隔的大型 CSV 文件 File1.csv
和 File2.csv
使用
Text::Diff
Perl 模块。
Perl 程序是从 .bat
文件中调用的,我将结果放在第三个文件中 Diff.csv
Perl
#!/usr/bin/env perl
use strict;
use warnings;
use Text::Diff;
my $diffs = diff $ARGV[0] => $ARGV[1];
$diffs =~ s/^(?:[^\n]*+\n){2}//;
$diffs =~ s/^(?:[\@ ][^\n]*+)?+\n//mg;
print $diffs;
这就是我调用 Perl 脚本的方式:
perl "C:\diffBetweenTwoFiles.pl" "C:\File1.csv" "C:\File2.csv" > "C:\Diff.csv"
CSV 文件中的其中一列是 Name
。
目前结果列出了所有列中的值发生变化的所有行,但我只想列出新的 Name
行。
例如:
File1.csv
"Name","DOB","Address"
"One","1/1/01","5 Stock Rd"
"Two","1/2/02","1 Research Rd"
File2.csv
"Name","DOB","Address"
"One","1/1/01","5 Stock Rd"
"Two","1/2/02","111 Research Rd"
"Three","1/3/03","3 Bold Rd"
目前,结果列出了这些(它包括 "Two" 因为它的地址已更改):
"Name","DOB","Address"
"Two","1/2/02","111 Research Rd"
"Three","1/3/03","3 Bold Rd"
但是,我只希望结果像这样列出新的 "Name":
"Name","DOB","Address"
"Three","1/3/03","3 Bold Rd"
如何在 Perl 或 Powershell 脚本中执行此操作?
在 Perl 中使用 Text::CSV
use warnings;
use strict;
use feature 'say';
use Text::CSV;
my ($file_old, $file_new, $file_diff) =
map { $_ . '.csv' } qw(File1 File2 Diff);
my $csv = Text::CSV->new ( { binary => 1 } )
or die "Cannot use CSV: ".Text::CSV->error_diag();
my ($old, $header) = get_lines($csv, $file_old, 1);
my $new = get_lines($csv, $file_new);
my @lines_with_new_names = @{ new_names($old, $new) };
open my $fh, '>', $file_diff or die "Can't open $file_diff: $!";
$csv->say($fh, $header);
$csv->say($fh, $_) for @lines_with_new_names; # or print with eol set
sub new_names {
my ($old, $new) = @_;
my %old = map { $_->[0] => 1 } @$old;
return [ map { (!exists $old{$_->[0]}) ? $_ : () } @$new ];
}
sub get_lines {
my ($csv, $file, $return_header) = @_;
open my $fh, '<', $file or die "Can't open $file $!";
my $header = $csv->getline($fh); # remove the header line
return ($return_header)
? ( $csv->getline_all($fh), $header )
: $csv->getline_all($fh);
}
这会打印出与所提供样本的正确差异。
带有old
标记的变量名与行数较少的文件相关,另一个是new
。 "Name" 列被认为是第一个。
评论
getline_all
方法return是所有行的数组引用,其中每一行都是一个包含所有字段的数组引用。这是通过子程序完成的,也可以选择 return header 行。
此处另一个变量的可选return使得单个标量或列表是returned的不同,因此也可以使用[=22来处理=]内置
return wantarray ? ( LIST ) : scalar;
如果在列表上下文中调用子项,则return为真。因此,调用者通过在列表或标量上下文中调用子来决定,my ($v1, $v2) = f(...)
或 my $v = f(...)
,在这种情况下,调用中不需要标志。我选择了更明确的方式。
名字列表的不同是在new_names
子中产生的。首先,使用 "old" arrayref 中的所有名称进行查找哈希。然后 "new" arrayref 中的行被过滤,取那些在 "old" 中没有名字的行(哈希中没有这样的键),并在 arrayref [=18] 中编辑 return =].
散列的这种使用是查找数组之间差异的标准技术。
记录的用于打印的方法 say
不适用于我测试它的旧版本模块。在这种情况下,使用 print
并在构造函数中设置 eol。
由于您处理的大文件对您的内存限制造成压力,您可以尝试:
- 一次一行读取第一个 CSV 文件,并使用哈希表存储文件的名称条目。
- 一次读取第二个 CSV 文件并将其名称条目与第一个进行比较。
(已更新 基于评论)PowerShell 中的一个简单示例:
$output = New-Object System.Text.StringBuilder;
$file1 = @{};
$header = $null;
# $filePaths is two-element array with full path to CSV files
for ($i = 0; $i -lt $filePaths.Length; ++$i) {
$reader = New-Object System.IO.StreamReader($filePaths[$i]);
while (($line = $reader.ReadLine()) -ne $null) {
if ($line -match '\S') {
if ($header -eq $null) {
$header = $line;
$output.AppendLine($line) | Out-Null;
}
$name = ($line -split ',')[0];
switch ($i) {
0 { $file1.Add($name, $null); }
1 {
if (!$file1.ContainsKey($name)) {
$output.AppendLine($line) | Out-Null;
}
}
}
}
}
$reader.Dispose();
}
$output.ToString() | Out-File -FilePath $outPath;
我正在比较两个以逗号分隔的大型 CSV 文件 File1.csv
和 File2.csv
使用
Text::Diff
Perl 模块。
Perl 程序是从 .bat
文件中调用的,我将结果放在第三个文件中 Diff.csv
Perl
#!/usr/bin/env perl
use strict;
use warnings;
use Text::Diff;
my $diffs = diff $ARGV[0] => $ARGV[1];
$diffs =~ s/^(?:[^\n]*+\n){2}//;
$diffs =~ s/^(?:[\@ ][^\n]*+)?+\n//mg;
print $diffs;
这就是我调用 Perl 脚本的方式:
perl "C:\diffBetweenTwoFiles.pl" "C:\File1.csv" "C:\File2.csv" > "C:\Diff.csv"
CSV 文件中的其中一列是 Name
。
目前结果列出了所有列中的值发生变化的所有行,但我只想列出新的 Name
行。
例如:
File1.csv
"Name","DOB","Address"
"One","1/1/01","5 Stock Rd"
"Two","1/2/02","1 Research Rd"
File2.csv
"Name","DOB","Address"
"One","1/1/01","5 Stock Rd"
"Two","1/2/02","111 Research Rd"
"Three","1/3/03","3 Bold Rd"
目前,结果列出了这些(它包括 "Two" 因为它的地址已更改):
"Name","DOB","Address"
"Two","1/2/02","111 Research Rd"
"Three","1/3/03","3 Bold Rd"
但是,我只希望结果像这样列出新的 "Name":
"Name","DOB","Address"
"Three","1/3/03","3 Bold Rd"
如何在 Perl 或 Powershell 脚本中执行此操作?
在 Perl 中使用 Text::CSV
use warnings;
use strict;
use feature 'say';
use Text::CSV;
my ($file_old, $file_new, $file_diff) =
map { $_ . '.csv' } qw(File1 File2 Diff);
my $csv = Text::CSV->new ( { binary => 1 } )
or die "Cannot use CSV: ".Text::CSV->error_diag();
my ($old, $header) = get_lines($csv, $file_old, 1);
my $new = get_lines($csv, $file_new);
my @lines_with_new_names = @{ new_names($old, $new) };
open my $fh, '>', $file_diff or die "Can't open $file_diff: $!";
$csv->say($fh, $header);
$csv->say($fh, $_) for @lines_with_new_names; # or print with eol set
sub new_names {
my ($old, $new) = @_;
my %old = map { $_->[0] => 1 } @$old;
return [ map { (!exists $old{$_->[0]}) ? $_ : () } @$new ];
}
sub get_lines {
my ($csv, $file, $return_header) = @_;
open my $fh, '<', $file or die "Can't open $file $!";
my $header = $csv->getline($fh); # remove the header line
return ($return_header)
? ( $csv->getline_all($fh), $header )
: $csv->getline_all($fh);
}
这会打印出与所提供样本的正确差异。
带有old
标记的变量名与行数较少的文件相关,另一个是new
。 "Name" 列被认为是第一个。
评论
getline_all
方法return是所有行的数组引用,其中每一行都是一个包含所有字段的数组引用。这是通过子程序完成的,也可以选择 return header 行。此处另一个变量的可选return使得单个标量或列表是returned的不同,因此也可以使用[=22来处理=]内置
return wantarray ? ( LIST ) : scalar;
如果在列表上下文中调用子项,则return为真。因此,调用者通过在列表或标量上下文中调用子来决定,
my ($v1, $v2) = f(...)
或my $v = f(...)
,在这种情况下,调用中不需要标志。我选择了更明确的方式。名字列表的不同是在
new_names
子中产生的。首先,使用 "old" arrayref 中的所有名称进行查找哈希。然后 "new" arrayref 中的行被过滤,取那些在 "old" 中没有名字的行(哈希中没有这样的键),并在 arrayref [=18] 中编辑 return =].散列的这种使用是查找数组之间差异的标准技术。
记录的用于打印的方法 say
不适用于我测试它的旧版本模块。在这种情况下,使用 print
并在构造函数中设置 eol。
由于您处理的大文件对您的内存限制造成压力,您可以尝试:
- 一次一行读取第一个 CSV 文件,并使用哈希表存储文件的名称条目。
- 一次读取第二个 CSV 文件并将其名称条目与第一个进行比较。
(已更新 基于评论)PowerShell 中的一个简单示例:
$output = New-Object System.Text.StringBuilder;
$file1 = @{};
$header = $null;
# $filePaths is two-element array with full path to CSV files
for ($i = 0; $i -lt $filePaths.Length; ++$i) {
$reader = New-Object System.IO.StreamReader($filePaths[$i]);
while (($line = $reader.ReadLine()) -ne $null) {
if ($line -match '\S') {
if ($header -eq $null) {
$header = $line;
$output.AppendLine($line) | Out-Null;
}
$name = ($line -split ',')[0];
switch ($i) {
0 { $file1.Add($name, $null); }
1 {
if (!$file1.ContainsKey($name)) {
$output.AppendLine($line) | Out-Null;
}
}
}
}
}
$reader.Dispose();
}
$output.ToString() | Out-File -FilePath $outPath;