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

时间:2019-05-22 02:34:13

标签: python perl awk sed large-data

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

示例映射: WaterPumpRoutine()

示例输入:value1,value2

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

请注意,该值可能在行中的不同位置,出现的次数超过1。

我当前的方法是使用AWK:

field1,field2,**value2**,field4

但是,这需要很长时间。

还有其他方法可以使此速度更快吗?可以使用多种工具(Unix,AWK,Sed,Perl,Python等)

4 个答案:

答案 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。

如果您提供了地图文件以外的一个或多个文件名,它将替换原位文件(尽管会留下备份)。