Как быстро найти и заменить много элементов в списке без замены ранее замененных элементов в BASH?

#perl #bash #optimization #replace #sed

#perl #bash #оптимизация #заменить #sed

Вопрос:

Я хочу выполнить около многих операций поиска и замены над некоторым текстом. У меня есть файл CSV в формате UTF-8, содержащий то, что нужно найти (в первом столбце) и чем его заменить (во втором столбце), упорядоченный от самого длинного к самому короткому.

Например.:

 orange,fruit2
carrot,vegetable1
apple,fruit3
pear,fruit4
ink,item1
table,item2
  

Исходный файл:

 "I like to eat apples and carrots"
  

Результирующий выходной файл:

 "I like to eat fruit3s and vegetable1s."
  

Однако я хочу убедиться, что если одна часть текста уже была заменена, она не будет мешать тексту, который уже был заменен. Другими словами, я не хочу, чтобы это выглядело так (оно соответствовало «таблице» из vegetable1):

 "I like to eat fruit3s and vegeitem21s."
  

В настоящее время я использую этот метод, который довольно медленный, потому что мне приходится выполнять весь поиск и замену дважды:

(1) Преобразуйте CSV в три файла, например:

 a.csv     b.csv   c.csv
orange    0001    fruit2
carrot    0002    vegetable1
apple     0003    fruit3
pear      0004    fruit4
ink       0005    item1
table     0006    item 2
  

(2) Затем замените все элементы из a.csv in file.txt на соответствующий столбец в b.csv , используя ZZZ вокруг слов, чтобы убедиться, что позже не будет ошибки при сопоставлении чисел:

 a=1
b=`wc -l < ./a.csv`
while [ $a -le $b ]
do
    for i in `sed -n "$a"p ./b.csv`; do
        for j in `sed -n "$a"p ./a.csv`; do
            sed -i "s/$i/ZZZ$jZZZ/g" ./file.txt
            echo "Instances of '"$i"' replaced with '"ZZZ$jZZZ"' ("$a"/"$b")."
            a=`expr $a   1`
            done
    done
done
  

(3) Затем снова запустите этот же скрипт, но для замены ZZZ0001ZZZ на fruit2 from c.csv .

Выполнение первой замены занимает около 2 часов, но поскольку я должен запустить этот код дважды, чтобы избежать редактирования уже замененных элементов, это занимает в два раза больше времени. Есть ли более эффективный способ запуска поиска и замены, который не выполняет замены уже замененного текста?

Комментарии:

1. На каком языке или технологии вы хотите это сделать?

2. В Linux. Я не имею в виду какой-либо конкретный язык, но мне нужно убедиться, что он поддерживает UTF-8.

3. сколько строк в каждом файле?

4. Редактируемые файлы и списки состоят из 100 000 строк каждый.

Ответ №1:

Вот решение perl, которое выполняет замену в «один этап».

 #!/usr/bin/perl
use strict;
my %map = (
       orange => "fruit2",
       carrot => "vegetable1",
       apple  => "fruit3",
       pear   => "fruit4",
       ink    => "item1",
       table  => "item2",
);
my $repl_rx = '(' . join("|", map { quotemeta } keys %map) . ')';
my $str = "I like to eat apples and carrots";
$str =~ s{$repl_rx}{$map{$1}}g;
print $str, "n";
  

Ответ №2:

У Tcl есть команда для выполнения именно этого: string map

 tclsh <<'END'
set map {
    "orange" "fruit2"
    "carrot" "vegetable1"
    "apple" "fruit3"
    "pear" "fruit4"
    "ink" "item1"
    "table" "item2"
}
set str "I like to eat apples and carrots"
puts [string map $map $str]
END
  
 I like to eat fruit3s and vegetable1s
  

Вот как это реализовать в bash (требуется bash v4 для ассоциативного массива)

 declare -A map=(
    [orange]=fruit2
    [carrot]=vegetable1
    [apple]=fruit3
    [pear]=fruit4
    [ink]=item1
    [table]=item2
)
str="I like to eat apples and carrots"
echo "$str"
i=0
while (( i < ${#str} )); do
    matched=false
    for key in "${!map[@]}"; do
        if [[ ${str:$i:${#key}} = $key ]]; then
            str=${str:0:$i}${map[$key]}${str:$((i ${#key}))}
            ((i =${#map[$key]}))
            matched=true
            break
        fi
    done
    $matched || ((i  ))
done
echo "$str"
  
 I like to eat apples and carrots
I like to eat fruit3s and vegetable1s
  

Это не будет быстрым.

Очевидно, что вы можете получить разные результаты, если упорядочите карту по-другому. На самом деле, я считаю, что порядок "${!map[@]}" не определен, поэтому вы можете указать порядок ключей явно:

 keys=(orange carrot apple pear ink table)
# ...
    for key in "${keys[@]}"; do
  

Ответ №3:

Одним из способов сделать это было бы выполнить двухфазную замену:

фаза 1:

s/orange/@@1##/
s/carrot/@@2##/
...

фаза 2:
s/@@1##/fruit2/
s/@@2###/vegetable1/
...

Маркеры @@1 ## следует выбирать так, чтобы они, конечно, не появлялись в исходном тексте или заменах.

Вот примерная реализация концепции в perl:

 #!/usr/bin/perl -w
#

my $repls = $ARGV[0];
die ("first parameter must be the replacement list file") unless defined ($repls);
my $tmpFmt = "@@@%d###";

open(my $replsFile, "<", $repls) || die("$!: $repls");
shift;

my @replsList;

my $i = 0;
while (<$replsFile>) {
    chomp;
    my ($from, $to) = /"([^"]*)","([^"]*)"/;
    if (defined($from) amp;amp; defined($to)) {
        push(@replsList, [$from, sprintf($tmpFmt,   $i), $to]);
    }
}

while (<>) {
    foreach my $r (@replsList) {
        s/$r->[0]/$r->[1]/g;
    }
    foreach my $r (@replsList) {
        s/$r->[1]/$r->[2]/g;
    }
    print;
}
  

Ответ №4:

Я бы предположил, что большая часть вашей медлительности связана с созданием такого количества команд sed, каждая из которых должна индивидуально обрабатывать весь файл. Некоторые незначительные корректировки вашего текущего процесса значительно ускорили бы это, запустив 1 sed на файл за шаг.

 a=1
b=`wc -l < ./a.csv`
while [ $a -le $b ]
do
    cmd=""
    for i in `sed -n "$a"p ./a.csv`; do
        for j in `sed -n "$a"p ./b.csv`; do
            cmd="$cmd ; s/$i/ZZZ${j}ZZZ/g"
            echo "Instances of '"$i"' replaced with '"ZZZ${j}ZZZ"' ("$a"/"$b")."
            a=`expr $a   1`
        done
    done

    sed -i "$cmd" ./file.txt
done
  

Ответ №5:

Выполнение этого дважды, вероятно, не ваша проблема. Если бы вам удалось сделать это только один раз, используя вашу базовую стратегию, это все равно заняло бы у вас час, верно? Вероятно, вам нужно использовать другую технологию или инструмент. Переключение на Perl, как указано выше, может сделать ваш код намного быстрее (попробуйте)

Но, продолжая путь других плакатов, следующим шагом может быть конвейеризация. Напишите небольшую программу, которая заменяет два столбца, затем запустите эту программу дважды, одновременно. При первом запуске строки в столбце 1 заменяются строками в столбце 2, при следующем — строки в столбце 2 заменяются строками в столбце 3.

Ваша командная строка будет выглядеть следующим образом

 cat input_file.txt | perl replace.pl replace_file.txt 1 2 | perl replace.pl replace_file.txt 2 3 > completely_replaced.txt
  

И заменить.pl был бы таким (похож на другие решения)

 #!/usr/bin/perl -w

my $replace_file = $ARGV[0];
my $before_replace_colnum = $ARGV[1] - 1;
my $after_replace_colnum = $ARGV[2] - 1;

open(REPLACEFILE, $replace_file) || die("couldn't open $replace_file: $!");

my @replace_pairs;

# read in the list of things to replace
while(<REPLACEFILE>) {
    chomp();

    my @cols = split /t/, $_;
    my $to_replace = $cols[$before_replace_colnum];
    my $replace_with = $cols[$after_replace_colnum];

    push @replace_pairs, [$to_replace, $replace_with];
}

# read input from stdin, do swapping
while(<STDIN>) {
    # loop over all replacement strings
    foreach my $replace_pair (@replace_pairs) {
        my($to_replace,$replace_with) = @{$replace_pair};
        $_ =~ s/${to_replace}/${replace_with}/g;
    }
    print STDOUT $_;
}
  

Комментарии:

1. cat действительно бесполезно, и только одного perl должно быть достаточно.

2. два perl позволяют выполнять конвейерную обработку

3. Вы могли бы реализовать замену как подпрограмму и вызвать ее дважды в одном perl. На самом деле вам вообще не нужны каналы.

4. это будет намного медленнее, правда. ваш подход будет использовать только один процессор / ядро.

5. Возможно, вы правы. Я собираюсь провести несколько тестов производительности… Но cat все еще не требуется. 😉

Ответ №6:

Подход bash sed:

 count=0
bigfrom=""
bigto=""

while IFS=, read from to; do
   read countmd5sum x < <(md5sum <<< $count)
   count=$(( $count   1 ))
   bigfrom="$bigfrom;s/$from/$countmd5sum/g"
   bigto="$bigto;s/$countmd5sum/$to/g"
done < replace-list.csv

sed "${bigfrom:1}$bigto" input_file.txt
  

Я выбрал md5sum, чтобы получить некоторый уникальный токен. Но для генерации такого токена также можно использовать какой-либо другой механизм; например, чтение из /dev/urandom или shuf -n1 -i 10000000-20000000

Ответ №7:

Подход awk sed:

 awk -F, '{a[NR-1]="s/####"NR"####/"$2"/";print "s/"$1"/####"NR"####/"}; END{for (i=0;i<NR;i  )print a[i];}' replace-list.csv > /tmp/sed_script.sed
sed -f /tmp/sed_script.sed input.txt
  

Подход cat sed sed:

 cat -n replace-list.csv | sed -rn 'H;g;s|(.*)n *([0-9] ) *[^,]*,(.*)|1ns/####2####/3/|;x;s|.*n *([0-9] )[ t]*([^,] ).*|s/2/####1####/|p;${g;s/^n//;p}' > /tmp/sed_script.sed
sed -f /tmp/sed_script.sed input.txt
  

Механизм:

  1. Здесь сначала генерируется сценарий sed, использующий csv в качестве входного файла.
  2. Затем использует другой экземпляр sed для работы input.txt

Примечания:

  1. Созданный промежуточный файл — sed_script.sed можно использовать повторно, если входной файл csv не изменится.
  2. ####<number>#### выбирается как некоторый шаблон, которого нет во входном файле. Измените этот шаблон, если требуется.
  3. cat -n | это не UUOC 🙂

Ответ №8:

Это может сработать для вас (GNU sed):

 sed -r 'h;s/./amp;\n/g;H;x;s/([^,]*),.*,(.*)/s|1|2|g/;$s/$/;s|\n||g/' csv_file | sed -rf - original_file
  

Преобразуйте csv файл в sed скрипт. Хитрость здесь заключается в том, чтобы заменить строку подстановки на ту, которая не будет заменена повторно. В этом случае каждый символ в строке подстановки заменяется самим собой и a n . Наконец, как только все замены выполнены, n ‘ы удаляются, оставляя готовую строку.

Ответ №9:

Здесь уже есть много интересных ответов. Я публикую это, потому что использую несколько иной подход, делая некоторые большие предположения о данных для замены ( на основе выборочных данных ).:

  1. Слова для замены не содержат пробелов
  2. Слова заменяются на основе самого длинного, точно совпадающего префикса
  3. Каждое слово, подлежащее замене, точно представлено в формате csv

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

Он считывает файл «repl.csv» в ассоциативный массив ( см. BEGIN{} ), затем пытается сопоставить префиксы каждого слова, когда длина слова ограничена ограничениями длины ключа, стараясь по возможности избегать поиска в ассоциативном массиве:

 #!/bin/awk -f

BEGIN {
    while( getline repline < "repl.csv" ) {
        split( repline, replarr, "," )
        replassocarr[ replarr[1] ] = replarr[2]
            # set some bounds on the replace word sizes
        if( minKeyLen == 0 || length( replarr[1] ) < minKeyLen )
            minKeyLen = length( replarr[1] )
        if( maxKeyLen == 0 || length( replarr[1] ) > maxKeyLen )
            maxKeyLen = length( replarr[1] )
    }
    close( "repl.csv" )
}

{
    i = 1
    while( i <= NF ) { print_word( $i, i == NF ); i   }
}

function print_word( w, end ) {
    wl = length( w )
    for( j = wl; j >= 0 amp;amp; prefix_len_bound( wl, j ); j-- ) {
        key = substr( w, 1, j )
        wl = length( key )
        if( wl >= minKeyLen amp;amp; key in replassocarr ) {
            printf( "%s%s%s", replassocarr[ key ],
                substr( w, j 1 ), !end ? " " : "n" )
            return
        }
    }
    printf( "%s%s", w, !end ? " " : "n" )
}

function prefix_len_bound( len, jlen ) {
    return len >= minKeyLen amp;amp; (len <= maxKeyLen || jlen > maxKeylen)
}
  

На основе ввода, подобного:

 I like to eat apples and carrots
orange you glad to see me
Some people eat pears while others drink ink
  

Это дает результат, подобный:

 I like to eat fruit3s and vegetable1s
fruit2 you glad to see me
Some people eat fruit4s while others drink item1
  

Конечно, любая «экономия», связанная с отсутствием поиска в replassocarr, исчезает, когда слова, подлежащие замене, достигают длины = 1 или если средняя длина слова намного больше, чем слова для замены.

Комментарии:

1. Я заметил, но не редактировал пример, что цикл print_word() действительно должен быть переработан так, чтобы просматривались только substr() ы, которые привязаны к максимальному и минимальному значению ключа. Прямо сейчас, это тратит некоторое время с конца более длинных слов.