Unix/Perl/Python:大数据集上的替代列表



>我有一个大约 13491 个键/值对的映射文件,我需要使用它来将键替换为大约 500000 行的数据集中的值,该数据集分为 25 个不同的文件。

示例映射:value1,value2

示例输入:field1,field2,**value1**,field4

示例输出:field1,field2,**value2**,field4

请注意,该值可能位于出现 1 次以上的行的不同位置。

我目前的做法是AWK:

awk -F, 'NR==FNR { a[$1]=$2 ; next } { for (i in a) gsub(i, a[i]); print }' mapping.txt file1.txt > file1_mapped.txt

但是,这需要很长时间。

有没有其他方法可以使其更快?可以使用各种工具(Unix,AWK,Sed,Perl,Python等)

注意有关使用Text::CSV模块解析文件的版本,请参阅第二部分


将映射加载到哈希(字典)中,然后遍历文件并测试每个字段的哈希中是否存在这样的键,如果有,请替换为值。将每一行写出到临时文件中,完成后将其移动到新文件中(或覆盖已处理的文件)。 任何工具都必须或多或少地做到这一点。

使用 Perl,用一些小的编造文件进行测试

use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
my $file = shift;
die "Usage: $0 mapping-file data-filesn"  if not $file or not @ARGV;
my %map;
open my $fh, '<', $file or die "Can't open $file: $!";
while (<$fh>) { 
my ($key, $val) = map { s/^s+|s+$//gr } split /s*,s*/;  # see Notes
$map{$key} = $val;
}
my $outfile = "tmp.outfile.txt.$$";  # but better use File::Temp
foreach my $file (@ARGV) {
open my $fh_out, '>', $outfile or die "Can't open $outfile: $!";
open my $fh,     '<', $file    or die "Can't open $file: $!";
while (<$fh>) {
s/^s+|s+$//g;               # remove leading/trailing whitespace
my @fields = split /s*,s*/;
exists($map{$_}) && ($_=$map{$_}) for @fields;  # see Notes
say $fh_out join ',', @fields;
}   
close $fh_out;
# Change to commented out line once thoroughly tested
#move($outfile, $file) or die "can't move $outfile to $file: $!";
move($outfile, 'new_'.$file) or die "can't move $outfile: $!";
}

笔记。

  • 根据映射检查数据是为了提高效率:我们必须查看每个字段,无法逃避,但是我们只检查字段作为键(没有正则表达式)。为此,需要去除所有前导/尾随空格。因此,此代码可能会更改输出数据文件中的空格;如果由于某种原因这很重要,当然可以对其进行修改以保留原始空间。

  • 评论中提到,数据中的字段实际上可以通过额外的引号而有所不同。然后先提取可能的密钥

    for (@fields) {
    $_ = $map{$1}  if /"?([^"]*)/ and exists $map{$1};
    }
    

    这会在每次检查时启动正则表达式引擎,这会影响效率。它将有助于清理报价的输入 CSV 数据,并使用上面的代码运行,没有正则表达式。这可以通过使用 CSV 解析模块读取文件来完成;请参阅末尾的评论。

  • 对于低于 5.14 的 Perls,请替换

    my ($key, $val) = map { s/^s+|s+$//gr } split /s*,s*/;
    

    my ($key, $val) = map { s/^s+|s+$//g; $_ } split /s*,s*/;
    

    由于"非破坏性"/r修饰符仅在 v5.14 中引入

  • 如果您希望整个操作不会因为一个错误文件而死亡,请将or die ...替换为

    or do { 
    # print warning for whatever failed (warn "Can't open $file: $!";)
    # take care of filehandles and such if/as needed
    next;
    };
    

    并确保(可能记录和)查看输出。

这为效率改进留下了一些空间,但没有什么显着的。


用逗号分隔字段的数据可能(也可能不是)有效的 CSV。由于该问题根本不会解决这个问题,也不会报告问题,因此不太可能在数据文件中使用 CSV 数据格式的任何属性(嵌入在数据中的分隔符、受保护的引号)。

但是,使用支持完整 CSV 的模块(如 Text::CSV)读取这些文件仍然是一个好主意。这也使事情变得更容易,通过处理额外的空格和引号,并交给我们清理的字段。 所以这里是 - 与上面相同,但使用模块来解析文件

use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
use Text::CSV;
my $file = shift;
die "Usage: $0 mapping-file data-filesn"  if not $file or not @ARGV;

my $csv = Text::CSV->new ( { binary => 1, allow_whitespace => 1 } ) 
or die "Cannot use CSV: " . Text::CSV->error_diag ();
my %map;
open my $fh, '<', $file or die "Can't open $file: $!";
while (my $line = $csv->getline($fh)) {
$map{ $line->[0] } = $line->[1]
}
my $outfile = "tmp.outfile.txt.$$";  # use File::Temp    
foreach my $file (@ARGV) {
open my $fh_out, '>', $outfile or die "Can't open $outfile: $!";
open my $fh,     '<', $file    or die "Can't open $file: $!";
while (my $line = $csv->getline($fh)) {
exists($map{$_}) && ($_=$map{$_}) for @$line;
say $fh_out join ',', @$line;
}
close $fh_out;
move($outfile, 'new_'.$file) or die "Can't move $outfile: $!";
}

现在我们根本不用担心空格或整体引号,这简化了事情。

虽然在没有实际数据文件的情况下很难可靠地比较这两种方法,但我对涉及"类似"处理的(虚构的)大数据文件进行了基准测试。使用Text::CSV进行解析的代码运行速度大致相同,或者(最多)快 50%。

构造函数选项allow_whitespace使它删除多余的空格,可能与名称可能暗示的内容相反,就像我在上面手动所做的那样。(另请参阅allow_loose_quotes和相关选项。还有更多,请参阅文档。如果已安装,Text::CSV默认为 Text::CSV_XS。

您在 500,000 个输入行中的每一行上都执行了 13,491 个gsub()- 这几乎是 70 亿个全行正则表达式搜索/替换总数。所以是的,这需要一些时间,而且几乎可以肯定的是,由于一个 gsub() 被下一个 gsub() 更改和/或您获得部分替换,它以您没有注意到的方式损坏您的数据!

我在评论中看到您的某些字段可以用双引号括起来。如果这些字段不能包含逗号或换行符,并且假设您想要完整的字符串匹配,那么这就是编写方法:

$ cat tst.awk
BEGIN { FS=OFS="," }
NR==FNR {
map[$1] = $2
map["""$1"""] = """$2"""
next
}
{
for (i=1; i<=NF; i++) {
if ($i in map) {
$i = map[$i]
}
}
print
}

我在功率不足的笔记本电脑上,在一个包含 13,500 个条目的映射文件和一个包含 500,000 行的输入文件上测试了上述内容,该文件在 cygwin 的大多数行上具有多个匹配项,并在大约 1 秒内完成:

$ wc -l mapping.txt
13500 mapping.txt
$ wc -l file500k
500000 file500k
$ time awk -f tst.awk mapping.txt file500k > /dev/null
real    0m1.138s
user    0m1.109s
sys     0m0.015s

如果这不能有效地完成您想要的,那么请编辑您的问题以提供 MCVE 和更清晰的要求,请参阅我在您的问题下的评论。

下面有一些评论建议OP需要处理真实的CSV数据,而问题说:

请注意,该值可能位于出现 1 次以上的行的不同位置。

我认为这意味着这些是行,而不是 CSV 数据,并且需要基于正则表达式的解决方案。OP还在上面的评论中证实了这一解释。

但是,如其他答案中所述,将数据分解为字段并简单地在地图中查找替换项会更快。

#!/usr/bin/env perl
use strict;
use warnings;
# Load mappings.txt into a Perl
# Hash %m.
#
open my $mh, '<', './mappings.txt'
or die "open: $!";
my %m = ();
while ($mh) {
chomp;
my @f = split ',';
$m{$f[0]} = $f[1];
}
# Load files.txt into a Perl
# Array @files.
#
open my $fh, '<', './files.txt';
chomp(my @files = $fh);
# Update each file line by line,
# using a temporary file similar
# to sed -i.
#
foreach my $file (@files) {
open my $fh, '<', $file
or die "open: $!";
open my $th, '>', "$file.bak"
or die "open: $!";
while ($fh) {
foreach my $k (keys %m) {
my $v = $m[$k];
s/Q$k/$v/g;
}
print $th;
}
rename "$file.bak", $file
or die "rename: $!";
}

我当然假设您在mappings.txt中有映射,files.txt中有文件列表。

根据您的评论,您有适当的 CSV。以下内容可正确处理从映射文件读取、从数据文件读取以及写入数据文件时的引用和转义。

似乎您想要匹配整个字段。下面这样做。它甚至支持包含逗号(,)和/或引号(")的字段。它使用哈希查找进行比较,这比正则表达式匹配快得多。

#!/usr/bin/perl
use strict;
use warnings;
use feature qw( say );
use Text::CSV_XS qw( );
my $csv = Text::CSV_XS->new({ auto_diag => 2, binary => 1 });
sub process {
my ($map, $in_fh, $out_fh) = @_;
while ( my $row = $csv->getline($in_fh) ) {
$csv->say($out_fh, [ map { $map->{$_} // $_ } @$row ]);
}
}
die "usage: $0 {map} [{file} [...]]n"
if @ARGV < 1;
my $map_qfn = shift;
my %map;
{
open(my $fh, '<', $map_qfn)
or die("Can't open "$map_qfn": $!n");
while ( my $row = $csv->getline($fh) ) {
$map{$row->[0]} = $row->[1];
}
}
if (@ARGV) {
for my $qfn (@ARGV) {
open(my $in_fh, '<', $qfn)
or warn("Can't open "$qfn": $!n"), next;
rename($qfn, $qfn."~")
or warn("Can't rename "$qfn": $!n"), next;
open(my $out_fh, '>', $qfn)
or warn("Can't create "$qfn": $!n"), next;
eval { process(%map, $in_fh, $out_fh); 1 }
or warn("Error processing "$qfn": $@"), next;
close($out_fh)
or warn("Error writing to "$qfn": $!n"), next;
}
} else {
eval { process(%map, *STDIN, *STDOUT); 1 }
or warn("Error processing: $@");
close(*STDOUT)
or warn("Error writing to STDOUT: $!n");
}

如果除映射文件外未提供任何文件名,它将从 STDIN 读取并输出到 STDOUT。

如果在映射文件之外提供一个或多个文件名,则会就地替换这些文件(尽管会留下备份)。

最新更新