如何快速查找和替换列表中的许多项目而不更换BASH中以前替换的项目?

时间:2011-11-05 08:18:39

标签: perl bash optimization replace sed

我想在一些文本上执行许多查找和替换操作。我有一个UTF-8 CSV文件,其中包含要查找的内容(在第一列中)以及替换内容(在第二列中),从最长到最短排列。

E.g:

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中的“table”匹配):

"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中的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$j\ZZZ/g" ./file.txt
            echo "Instances of '"$i"' replaced with '"ZZZ$j\ZZZ"' ("$a"/"$b")."
            a=`expr $a + 1`
            done
    done
done

(3)然后再次运行相同的脚本,但要将ZZZ0001ZZZ替换为fruit2的{​​{1}}。

运行第一次替换需要大约2个小时,但由于我必须运行此代码两次以避免编辑已经替换的项目,因此需要两倍的时间。是否有更有效的方法来运行查找和替换不会对已替换的文本执行替换?

9 个答案:

答案 0 :(得分:6)

这是一个在“一个阶段”中进行替换的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";

答案 1 :(得分:3)

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

答案 2 :(得分:2)

这样做的一种方法是进行两阶段替换:

phase 1:

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

phase 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) && 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;
}

答案 3 :(得分:1)

做两次可能你的问题。如果你设法使用基本策略一次,它仍然需要一个小时,对吗?您可能需要使用不同的技术或工具。如上所述,切换到Perl可能会使代码更快(尝试一下)

但是继续走下其他海报的道路,下一步可能是流水线。编写一个替换两列的小程序,然后同时运行该程序两次。第一次运行使用column2中的字符串交换column1中的字符串,然后使用column3中的字符串交换column2中的字符串。

您的命令行将是这样的

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

而且replace.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 $_;
}

答案 4 :(得分:1)

我猜你的大部分迟缓都来自创建如此多的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 :(得分:1)

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/urandomshuf -n1 -i 10000000-20000000

阅读

答案 6 :(得分:1)

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]+) *[^,]*,(.*)|\1\ns/####\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. 这里,它首先使用csv作为输入文件生成sed脚本。
  2. 然后使用另一个sed实例来操作input.txt
  3. 注意:

    1. 生成的中间文件 - sed_script.sed可以再次重复使用,除非输入的csv文件发生变化。
    2. 选择
    3. ####<number>####作为某种模式,输入文件中不存在该模式。如果需要,请更改此模式。
    4. cat -n |不是UUOC:)

答案 7 :(得分:1)

这可能适合你(GNU sed):

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

csv文件转换为sed脚本。这里的技巧是将替换字符串替换为不会被重新替换的字符串。在这种情况下,替换字符串中的每个字符都由其自身和\n替换。最后,一旦完成所有替换,\n将被移除,留下完成的字符串。

答案 8 :(得分:1)

这里有很多很酷的答案。我发布这个是因为我采用了稍微不同的方法,对要替换的数据做了一些大的假设(基于样本数据):

  1. 要替换的单词不包含空格
  2. 根据最长的,完全匹配的前缀
  3. 替换单词
  4. 要替换的每个单词都在csv
  5. 中完全表示

    这是一次通过,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 && prefix_len_bound( wl, j ); j-- ) {
            key = substr( w, 1, j )
            wl = length( key )
            if( wl >= minKeyLen && 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 && (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
    

    当然,当要替换的单词长度= 1或者平均单词长度远远超过要替换的单词时,任何不看替换文件的“节省”都会消失。