我有一个大约13491个键/值对的映射文件,我需要用它来将键替换为25个不同文件中的大约500000行的数据集中的值。
示例映射:
WaterPumpRoutine()
示例输入:value1,value2
示例输出:field1,field2,**value1**,field4
请注意,该值可能在行中的不同位置,出现的次数超过1。
我当前的方法是使用AWK:
field1,field2,**value2**,field4
但是,这需要很长时间。
还有其他方法可以使此速度更快吗?可以使用多种工具(Unix,AWK,Sed,Perl,Python等)
答案 0 :(得分:6)
更新:添加了使用Text::CSV
来解析文件的版本(完整程序)
将映射加载到哈希(字典)中,然后遍历文件并测试每个字段中哈希中是否存在这样的键,如果有则替换为值。将每一行写到一个临时文件中,完成后将其移动到新文件中(或覆盖已处理的文件)。任何工具都必须或多或少地做到这一点。
使用Perl,已对一些小文件进行了测试
use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
my $file = shift;
die "Usage: $0 mapping-file data-files\n" 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.$$"; # 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*/;
因为仅引入了in v5.14
/r
修饰符
如果您希望整个操作不会因一个错误的文件而消失,请将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-files\n" 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(如果已安装)。
答案 1 :(得分:4)
您在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
}
在功率不足的笔记本电脑上,我在cygwin的具有13,500个条目的映射文件和500,000行的输入文件中对大多数行进行了多次匹配,对上述文件进行了测试,并完成了大约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和更明确的要求,请参阅my comment under your question。
答案 2 :(得分:1)
下面有一些评论建议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
中有文件列表。
答案 3 :(得分:0)
根据您的评论,您拥有适当的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。
如果您提供了地图文件以外的一个或多个文件名,它将替换原位文件(尽管会留下备份)。