使用 Perl 或 Powershell,如何比较 2 个 CSV 文件并只获取新行?

Using Perl or Powershell, how to compare 2 CSV files and get only the new rows?

我正在比较两个以逗号分隔的大型 CSV 文件 File1.csvFile2.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

由于您处理的大文件对您的内存限制造成压力,您可以尝试:

  1. 一次一行读取第一个 CSV 文件,并使用哈希表存储文件的名称条目。
  2. 一次读取第二个 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;