Unix/Perl/Python:大数据集上的替代列表
Unix/Perl/Python: substitute list on big data set
我有一个包含大约 13491 key/value 对的映射文件,我需要用它来将密钥替换为数据集中大约 500000 行的值,这些数据集分为 25 个不同的文件。
映射示例:
value1,value2
示例输入:field1,field2,**value1**,field4
示例输出:field1,field2,**value2**,field4
请注意,该值可能位于出现次数超过 1 次的行中的不同位置。
我目前的方法是使用 AWK:
awk -F, 'NR==FNR { a[]= ; 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,测试了一些小 made-up 文件
use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
my $file = shift;
die "Usage: [=10=] 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.$$"; # 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: $!";
}
注释。
针对映射的数据检查是为了提高效率而编写的:我们必须查看每个字段,这是不可避免的,但是我们只检查字段作为键(没有正则表达式)。为此,需要删除所有 leading/trailing 空格。因此这段代码可能会改变输出数据文件中的空格;如果出于某种原因这很重要,当然可以对其进行修改以保留原始空间。
评论中提到数据中的字段实际上可以不同,因为有额外的引号。然后先提取would-be键
for (@fields) {
$_ = $map{} if /"?([^"]*)/ and exists $map{};
}
这会在每次检查时启动正则表达式引擎,这会影响效率。这将有助于清理引号的输入 CSV 数据,并且 运行 使用上面的代码,没有正则表达式。这可以通过使用 CSV-parsing 模块读取文件来完成;评论见文末
对于早于 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*/;
因为仅引入了“non-destructive”/r
修饰符in 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)读取这些文件仍然是个好主意。通过处理额外的空格和引号并交给我们 cleaned-up 字段,这也使事情变得更容易。就是这样——和上面一样,但是使用模块来解析文件
use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
use Text::CSV;
my $file = shift;
die "Usage: [=15=] 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: $!";
}
现在我们根本不必担心空格或整体引号,这让事情变得简单了一点。
虽然在没有实际数据文件的情况下很难可靠地比较这两种方法,但我对涉及“相似”处理的 (made-up) 大数据文件进行了基准测试。使用 Text::CSV
解析 运行 的代码大致相同,或者(最多)快 50%。
构造函数选项 allow_whitespace makes it remove extra spaces, perhaps contrary to what the name may imply, as I do by hand above. (Also see allow_loose_quotes
and related options.) There is far more, see docs. The Text::CSV
defaults to Text::CSV_XS,如果已安装。
下面有一些评论表明 OP 需要处理真实的 CSV 数据,而问题是:
Please note that the value could be in different places on the line with more than 1 occurrence.
我认为这意味着这些是行,而不是 CSV 数据,并且需要 regex-based 解决方案。 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。当从映射文件读取、从数据文件读取和写入数据文件时,以下正确处理引号和转义。
您似乎想要匹配整个字段。以下是这样做的。它甚至支持包含逗号 (,
) and/or 引号 ("
) 的字段。它使用哈希查找进行比较,这比正则表达式匹配快得多。
#!/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: [=10=] {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。
如果您在地图文件之外提供一个或多个文件名,它会替换文件 in-place(尽管会留下备份)。
您在 500,000 条输入行中的每一行都执行了 13,491 gsub()
s - 总共将近 70 亿条 full-line 正则表达式 search/replaces。所以是的,这需要一些时间,而且它几乎肯定会以您没有注意到的方式破坏您的数据,因为一个 gsub() 的结果被下一个 gsub() 更改 and/or 你得到部分替换!
我在评论中看到,您的某些字段可以用双引号引起来。如果这些字段不能包含逗号或换行符,并且假设您想要完整的字符串匹配,那么这就是如何编写它:
$ cat tst.awk
BEGIN { FS=OFS="," }
NR==FNR {
map[] =
map["\"""\""] = "\"""\""
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 and clearer requirements, see 。
我有一个包含大约 13491 key/value 对的映射文件,我需要用它来将密钥替换为数据集中大约 500000 行的值,这些数据集分为 25 个不同的文件。
映射示例:
value1,value2
示例输入:field1,field2,**value1**,field4
示例输出:field1,field2,**value2**,field4
请注意,该值可能位于出现次数超过 1 次的行中的不同位置。
我目前的方法是使用 AWK:
awk -F, 'NR==FNR { a[]= ; 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,测试了一些小 made-up 文件
use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
my $file = shift;
die "Usage: [=10=] 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.$$"; # 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: $!";
}
注释。
针对映射的数据检查是为了提高效率而编写的:我们必须查看每个字段,这是不可避免的,但是我们只检查字段作为键(没有正则表达式)。为此,需要删除所有 leading/trailing 空格。因此这段代码可能会改变输出数据文件中的空格;如果出于某种原因这很重要,当然可以对其进行修改以保留原始空间。
评论中提到数据中的字段实际上可以不同,因为有额外的引号。然后先提取would-be键
for (@fields) { $_ = $map{} if /"?([^"]*)/ and exists $map{}; }
这会在每次检查时启动正则表达式引擎,这会影响效率。这将有助于清理引号的输入 CSV 数据,并且 运行 使用上面的代码,没有正则表达式。这可以通过使用 CSV-parsing 模块读取文件来完成;评论见文末
对于早于 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*/;
因为仅引入了“non-destructive”
/r
修饰符in 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)读取这些文件仍然是个好主意。通过处理额外的空格和引号并交给我们 cleaned-up 字段,这也使事情变得更容易。就是这样——和上面一样,但是使用模块来解析文件
use warnings;
use strict;
use feature 'say';
use File::Copy qw(move);
use Text::CSV;
my $file = shift;
die "Usage: [=15=] 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: $!";
}
现在我们根本不必担心空格或整体引号,这让事情变得简单了一点。
虽然在没有实际数据文件的情况下很难可靠地比较这两种方法,但我对涉及“相似”处理的 (made-up) 大数据文件进行了基准测试。使用 Text::CSV
解析 运行 的代码大致相同,或者(最多)快 50%。
构造函数选项 allow_whitespace makes it remove extra spaces, perhaps contrary to what the name may imply, as I do by hand above. (Also see allow_loose_quotes
and related options.) There is far more, see docs. The Text::CSV
defaults to Text::CSV_XS,如果已安装。
下面有一些评论表明 OP 需要处理真实的 CSV 数据,而问题是:
Please note that the value could be in different places on the line with more than 1 occurrence.
我认为这意味着这些是行,而不是 CSV 数据,并且需要 regex-based 解决方案。 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。当从映射文件读取、从数据文件读取和写入数据文件时,以下正确处理引号和转义。
您似乎想要匹配整个字段。以下是这样做的。它甚至支持包含逗号 (,
) and/or 引号 ("
) 的字段。它使用哈希查找进行比较,这比正则表达式匹配快得多。
#!/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: [=10=] {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。
如果您在地图文件之外提供一个或多个文件名,它会替换文件 in-place(尽管会留下备份)。
您在 500,000 条输入行中的每一行都执行了 13,491 gsub()
s - 总共将近 70 亿条 full-line 正则表达式 search/replaces。所以是的,这需要一些时间,而且它几乎肯定会以您没有注意到的方式破坏您的数据,因为一个 gsub() 的结果被下一个 gsub() 更改 and/or 你得到部分替换!
我在评论中看到,您的某些字段可以用双引号引起来。如果这些字段不能包含逗号或换行符,并且假设您想要完整的字符串匹配,那么这就是如何编写它:
$ cat tst.awk
BEGIN { FS=OFS="," }
NR==FNR {
map[] =
map["\"""\""] = "\"""\""
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 and clearer requirements, see