Unix / Perl / Python: список подстановок для большого набора данных - PullRequest
6 голосов
/ 22 мая 2019

У меня есть файл сопоставления, содержащий около 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 и т. Д.)

Ответы [ 4 ]

6 голосов
/ 22 мая 2019

Обновление Добавлена ​​версия (полная программа), использующая 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;см. комментарий в конце.

  • Для Perls ранее 5.14 замените

    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-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 , если установлено.

4 голосов
/ 22 мая 2019

Вы делаете 13 491 gsub() с на каждую из ваших 500 000 строк ввода - это почти 7 миллиардов полных строк поиска / замены регулярных выражений.Так что да, это займет некоторое время, и это почти наверняка повредит ваши данные способами, которые вы просто не заметили, так как результат одного 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 и более четкие требования, см. мой комментарий к вашему вопросу .

1 голос
/ 22 мая 2019

Ниже приведен некоторый комментарий, предполагающий, что OP должен обрабатывать реальные данные CSV, тогда как вопрос говорит:

Обратите внимание, что значение может находиться в разных местах строки с более чем 1 вхождением.

Я понял, что это строки, а не данные CSV, и что требуется решение на основе регулярных выражений. ФП также подтвердил это толкование в комментарии выше.

Однако, как отмечалось в других ответах, быстрее разбить данные на поля и просто найти замену на карте.

#!/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.

0 голосов
/ 22 мая 2019

Согласно вашим комментариям, у вас есть правильный 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.

Если вы указываете одно или несколько имен файлов помимо файла карты, он заменяет файлы на месте (хотя оставляет резервную копию).

...